summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLee Rowlands2018-01-30 22:28:03 (GMT)
committerLee Rowlands2018-01-30 22:28:03 (GMT)
commit02f932546662854f23e5eacd4a905e80bf7c6697 (patch)
tree86bd06bc322e83580a8e21d3c7bedce00d184fa5
parent1455cb13cf92021cedbb1262767ddfe46f0e4d66 (diff)
Issue #2922033 by tim.plunkett, mtodor, larowlan, xjm, pameeela, EclipseGc, samuel.mortenson: Use the Layout Builder for EntityViewDisplays
-rw-r--r--core/config/schema/core.entity.schema.yml2
-rw-r--r--core/modules/layout_builder/config/schema/layout_builder.schema.yml39
-rw-r--r--core/modules/layout_builder/layout_builder.info.yml2
-rw-r--r--core/modules/layout_builder/layout_builder.install40
-rw-r--r--core/modules/layout_builder/layout_builder.module171
-rw-r--r--core/modules/layout_builder/layout_builder.services.yml5
-rw-r--r--core/modules/layout_builder/src/Cache/LayoutBuilderIsActiveCacheContext.php14
-rw-r--r--core/modules/layout_builder/src/Controller/LayoutBuilderController.php39
-rw-r--r--core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php488
-rw-r--r--core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplayStorage.php60
-rw-r--r--core/modules/layout_builder/src/Entity/LayoutEntityDisplayInterface.php36
-rw-r--r--core/modules/layout_builder/src/Field/LayoutSectionItemList.php14
-rw-r--r--core/modules/layout_builder/src/Form/AddBlockForm.php31
-rw-r--r--core/modules/layout_builder/src/Form/ConfigureBlockFormBase.php62
-rw-r--r--core/modules/layout_builder/src/Form/LayoutBuilderEntityViewDisplayForm.php94
-rw-r--r--core/modules/layout_builder/src/Form/RevertOverridesForm.php117
-rw-r--r--core/modules/layout_builder/src/Form/UpdateBlockForm.php21
-rw-r--r--core/modules/layout_builder/src/LayoutTempstoreRepository.php2
-rw-r--r--core/modules/layout_builder/src/OverridesSectionStorageInterface.php26
-rw-r--r--core/modules/layout_builder/src/Plugin/Block/FieldBlock.php21
-rw-r--r--core/modules/layout_builder/src/Plugin/Derivative/FieldBlockDeriver.php94
-rw-r--r--core/modules/layout_builder/src/Plugin/Derivative/LayoutBuilderLocalTaskDeriver.php36
-rw-r--r--core/modules/layout_builder/src/Routing/LayoutBuilderRoutes.php117
-rw-r--r--core/modules/layout_builder/src/Routing/LayoutBuilderRoutesTrait.php92
-rw-r--r--core/modules/layout_builder/src/Routing/SectionStorageDefaultsParamConverter.php53
-rw-r--r--core/modules/layout_builder/src/Section.php35
-rw-r--r--core/modules/layout_builder/src/SectionComponent.php27
-rw-r--r--core/modules/layout_builder/src/SectionStorageInterface.php7
-rw-r--r--core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php232
-rw-r--r--core/modules/layout_builder/tests/src/Functional/LayoutSectionTest.php60
-rw-r--r--core/modules/layout_builder/tests/src/FunctionalJavascript/FieldBlockTest.php3
-rw-r--r--core/modules/layout_builder/tests/src/Kernel/FieldBlockTest.php2
-rw-r--r--core/modules/layout_builder/tests/src/Kernel/LayoutBuilderCompatibilityTestBase.php5
-rw-r--r--core/modules/layout_builder/tests/src/Kernel/LayoutBuilderEntityViewDisplayTest.php51
-rw-r--r--core/modules/layout_builder/tests/src/Kernel/LayoutBuilderInstallTest.php36
-rw-r--r--core/modules/layout_builder/tests/src/Kernel/LayoutSectionItemListTest.php8
-rw-r--r--core/modules/layout_builder/tests/src/Kernel/SectionStorageTestBase.php26
-rw-r--r--core/modules/layout_builder/tests/src/Unit/LayoutBuilderRoutesTest.php314
-rw-r--r--core/modules/layout_builder/tests/src/Unit/LayoutTempstoreRepositoryTest.php113
-rw-r--r--core/modules/layout_builder/tests/src/Unit/SectionStorageDefaultsParamConverterTest.php134
40 files changed, 2322 insertions, 407 deletions
diff --git a/core/config/schema/core.entity.schema.yml b/core/config/schema/core.entity.schema.yml
index 73d84bc..57e5547 100644
--- a/core/config/schema/core.entity.schema.yml
+++ b/core/config/schema/core.entity.schema.yml
@@ -369,7 +369,7 @@ field.formatter.settings.entity_reference_label:
type: boolean
label: 'Link label to the referenced entity'
-block.settings.field_block:*:*:
+block.settings.field_block:*:*:*:
type: block_settings
mapping:
formatter:
diff --git a/core/modules/layout_builder/config/schema/layout_builder.schema.yml b/core/modules/layout_builder/config/schema/layout_builder.schema.yml
index b870007..682caa7 100644
--- a/core/modules/layout_builder/config/schema/layout_builder.schema.yml
+++ b/core/modules/layout_builder/config/schema/layout_builder.schema.yml
@@ -5,3 +5,42 @@ core.entity_view_display.*.*.*.third_party.layout_builder:
allow_custom:
type: boolean
label: 'Allow a customized layout'
+ sections:
+ type: sequence
+ sequence:
+ type: layout_builder.section
+
+layout_builder.section:
+ type: mapping
+ label: 'Layout section'
+ mapping:
+ layout_id:
+ type: string
+ label: 'Layout ID'
+ layout_settings:
+ type: layout_plugin.settings.[%parent.layout_id]
+ label: 'Layout settings'
+ components:
+ type: sequence
+ label: 'Components'
+ sequence:
+ type: layout_builder.component
+
+layout_builder.component:
+ type: mapping
+ label: 'Component'
+ mapping:
+ configuration:
+ type: block.settings.[id]
+ region:
+ type: string
+ label: 'Region'
+ uuid:
+ type: uuid
+ label: 'UUID'
+ weight:
+ type: integer
+ label: 'Weight'
+ additional:
+ type: ignore
+ label: 'Additional data'
diff --git a/core/modules/layout_builder/layout_builder.info.yml b/core/modules/layout_builder/layout_builder.info.yml
index e985911..d4ce72e 100644
--- a/core/modules/layout_builder/layout_builder.info.yml
+++ b/core/modules/layout_builder/layout_builder.info.yml
@@ -7,3 +7,5 @@ core: 8.x
dependencies:
- layout_discovery
- contextual
+ # @todo Discuss removing in https://www.drupal.org/project/drupal/issues/2935999.
+ - field_ui
diff --git a/core/modules/layout_builder/layout_builder.install b/core/modules/layout_builder/layout_builder.install
new file mode 100644
index 0000000..acb1e4f
--- /dev/null
+++ b/core/modules/layout_builder/layout_builder.install
@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * @file
+ * Contains install and update functions for Layout Builder.
+ */
+
+use Drupal\Core\Cache\Cache;
+use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
+use Drupal\layout_builder\Section;
+
+/**
+ * Implements hook_install().
+ */
+function layout_builder_install() {
+ $displays = LayoutBuilderEntityViewDisplay::loadMultiple();
+ /** @var \Drupal\layout_builder\Entity\LayoutEntityDisplayInterface[] $displays */
+ foreach ($displays as $display) {
+ // Create the first section from any existing Field Layout settings.
+ $field_layout = $display->getThirdPartySettings('field_layout');
+ if (isset($field_layout['id'])) {
+ $field_layout += ['settings' => []];
+ $display->appendSection(new Section($field_layout['id'], $field_layout['settings']));
+ }
+
+ // Sort the components by weight.
+ $components = $display->get('content');
+ uasort($components, 'Drupal\Component\Utility\SortArray::sortByWeightElement');
+ foreach ($components as $name => $component) {
+ $display->setComponent($name, $component);
+ }
+ $display->save();
+ }
+
+ // Clear the rendered cache to ensure the new layout builder flow is used.
+ // While in many cases the above change will not affect the rendered output,
+ // the cacheability metadata will have changed and should be processed to
+ // prepare for future changes.
+ Cache::invalidateTags(['rendered']);
+}
diff --git a/core/modules/layout_builder/layout_builder.module b/core/modules/layout_builder/layout_builder.module
index 895145d..80339c1 100644
--- a/core/modules/layout_builder/layout_builder.module
+++ b/core/modules/layout_builder/layout_builder.module
@@ -5,17 +5,14 @@
* Provides hook implementations for Layout Builder.
*/
-use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
-use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Plugin\Context\Context;
-use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\Core\Routing\RouteMatchInterface;
-use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;
-use Drupal\field\Entity\FieldConfig;
-use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\field\FieldConfigInterface;
+use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
+use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplayStorage;
+use Drupal\layout_builder\Form\LayoutBuilderEntityViewDisplayForm;
/**
* Implements hook_help().
@@ -52,17 +49,18 @@ function layout_builder_entity_type_alter(array &$entity_types) {
$entity_type->setLinkTemplate('layout-builder', $entity_type->getLinkTemplate('canonical') . '/layout');
}
}
+ $entity_types['entity_view_display']
+ ->setClass(LayoutBuilderEntityViewDisplay::class)
+ ->setStorageClass(LayoutBuilderEntityViewDisplayStorage::class)
+ ->setFormClass('edit', LayoutBuilderEntityViewDisplayForm::class);
}
/**
- * Removes the Layout Builder field both visually and from the #fields handling.
- *
- * This prevents any interaction with this field. It is rendered directly
- * in layout_builder_entity_view_alter().
- *
- * @internal
+ * Implements hook_form_FORM_ID_alter() for \Drupal\field_ui\Form\EntityFormDisplayEditForm.
*/
-function _layout_builder_hide_layout_field(array &$form) {
+function layout_builder_form_entity_form_display_edit_form_alter(&$form, FormStateInterface $form_state) {
+ // Hides the Layout Builder field. It is rendered directly in
+ // \Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay::buildMultiple().
unset($form['fields']['layout_builder__layout']);
$key = array_search('layout_builder__layout', $form['#fields']);
if ($key !== FALSE) {
@@ -71,140 +69,23 @@ function _layout_builder_hide_layout_field(array &$form) {
}
/**
- * Implements hook_form_FORM_ID_alter() for \Drupal\field_ui\Form\EntityFormDisplayEditForm.
- */
-function layout_builder_form_entity_form_display_edit_form_alter(&$form, FormStateInterface $form_state) {
- _layout_builder_hide_layout_field($form);
-}
-
-/**
- * Implements hook_form_FORM_ID_alter() for \Drupal\field_ui\Form\EntityViewDisplayEditForm.
- */
-function layout_builder_form_entity_view_display_edit_form_alter(&$form, FormStateInterface $form_state) {
- /** @var \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display */
- $display = $form_state->getFormObject()->getEntity();
- $entity_type = \Drupal::entityTypeManager()->getDefinition($display->getTargetEntityTypeId());
-
- _layout_builder_hide_layout_field($form);
-
- // @todo Expand to work for all view modes in
- // https://www.drupal.org/node/2907413.
- if (!in_array($display->getMode(), ['full', 'default'], TRUE)) {
- return;
- }
-
- $form['layout'] = [
- '#type' => 'details',
- '#open' => TRUE,
- '#title' => t('Layout options'),
- '#tree' => TRUE,
- ];
- // @todo Unchecking this box is a destructive action, this should be made
- // clear to the user in https://www.drupal.org/node/2914484.
- $form['layout']['allow_custom'] = [
- '#type' => 'checkbox',
- '#title' => t('Allow each @entity to have its layout customized.', [
- '@entity' => $entity_type->getSingularLabel(),
- ]),
- '#default_value' => $display->getThirdPartySetting('layout_builder', 'allow_custom', FALSE),
- ];
-
- $form['#entity_builders'][] = 'layout_builder_form_entity_view_display_edit_entity_builder';
-}
-
-/**
- * Entity builder for layout options on the entity view display form.
- *
- * @see layout_builder_form_entity_view_display_edit_form_alter()
- */
-function layout_builder_form_entity_view_display_edit_entity_builder($entity_type_id, EntityViewDisplayInterface $display, &$form, FormStateInterface &$form_state) {
- $new_value = (bool) $form_state->getValue(['layout', 'allow_custom'], FALSE);
- $display->setThirdPartySetting('layout_builder', 'allow_custom', $new_value);
-}
-
-/**
- * Implements hook_ENTITY_TYPE_presave().
- */
-function layout_builder_entity_view_display_presave(EntityViewDisplayInterface $display) {
- $original_value = isset($display->original) ? $display->original->getThirdPartySetting('layout_builder', 'allow_custom', FALSE) : FALSE;
- $new_value = $display->getThirdPartySetting('layout_builder', 'allow_custom', FALSE);
- if ($original_value !== $new_value) {
- $entity_type_id = $display->getTargetEntityTypeId();
- $bundle = $display->getTargetBundle();
-
- if ($new_value) {
- layout_builder_add_layout_section_field($entity_type_id, $bundle);
- }
- elseif ($field = FieldConfig::loadByName($entity_type_id, $bundle, 'layout_builder__layout')) {
- $field->delete();
- }
- }
-}
-
-/**
- * Adds a layout section field to a given bundle.
- *
- * @param string $entity_type_id
- * The entity type ID.
- * @param string $bundle
- * The bundle.
- * @param string $field_name
- * (optional) The name for the layout section field. Defaults to
- * 'layout_builder__layout'.
- *
- * @return \Drupal\field\FieldConfigInterface
- * A layout section field.
+ * Implements hook_field_config_insert().
*/
-function layout_builder_add_layout_section_field($entity_type_id, $bundle, $field_name = 'layout_builder__layout') {
- $field = FieldConfig::loadByName($entity_type_id, $bundle, $field_name);
- if (!$field) {
- $field_storage = FieldStorageConfig::loadByName($entity_type_id, $field_name);
- if (!$field_storage) {
- $field_storage = FieldStorageConfig::create([
- 'entity_type' => $entity_type_id,
- 'field_name' => $field_name,
- 'type' => 'layout_section',
- ]);
- $field_storage->save();
- }
-
- $field = FieldConfig::create([
- 'field_storage' => $field_storage,
- 'bundle' => $bundle,
- 'label' => t('Layout'),
- ]);
- $field->save();
- }
- return $field;
+function layout_builder_field_config_insert(FieldConfigInterface $field_config) {
+ // Clear the sample entity for this entity type and bundle.
+ /** @var \Drupal\Core\TempStore\SharedTempStore $tempstore */
+ $tempstore = \Drupal::service('tempstore.shared')->get('layout_builder.sample_entity');
+ $tempstore->delete($field_config->getTargetEntityTypeId() . '.' . $field_config->getTargetBundle());
+ \Drupal::service('plugin.manager.block')->clearCachedDefinitions();
}
/**
- * Implements hook_entity_view_alter().
+ * Implements hook_field_config_delete().
*/
-function layout_builder_entity_view_alter(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display) {
- if ($display->getThirdPartySetting('layout_builder', 'allow_custom', FALSE) && !$entity->layout_builder__layout->isEmpty()) {
- $contexts = \Drupal::service('context.repository')->getAvailableContexts();
- // @todo Use EntityContextDefinition after resolving
- // https://www.drupal.org/node/2932462.
- $contexts['layout_builder.entity'] = new Context(new ContextDefinition("entity:{$entity->getEntityTypeId()}", new TranslatableMarkup('@entity being viewed', ['@entity' => $entity->getEntityType()->getLabel()])), $entity);
- $sections = $entity->layout_builder__layout->getSections();
- foreach ($sections as $delta => $section) {
- $build['_layout_builder'][$delta] = $section->toRenderArray($contexts);
- }
-
- // If field layout is active, that is all that needs to be removed.
- if (\Drupal::moduleHandler()->moduleExists('field_layout') && isset($build['_field_layout'])) {
- unset($build['_field_layout']);
- return;
- }
-
- /** @var \Drupal\Core\Field\FieldDefinitionInterface[] $field_definitions */
- $field_definitions = \Drupal::service('entity_field.manager')->getFieldDefinitions($display->getTargetEntityTypeId(), $display->getTargetBundle());
- // Remove all display-configurable fields.
- foreach (array_keys($display->getComponents()) as $name) {
- if ($name !== 'layout_builder__layout' && isset($field_definitions[$name]) && $field_definitions[$name]->isDisplayConfigurable('view')) {
- unset($build[$name]);
- }
- }
- }
+function layout_builder_field_config_delete(FieldConfigInterface $field_config) {
+ // Clear the sample entity for this entity type and bundle.
+ /** @var \Drupal\Core\TempStore\SharedTempStore $tempstore */
+ $tempstore = \Drupal::service('tempstore.shared')->get('layout_builder.sample_entity');
+ $tempstore->delete($field_config->getTargetEntityTypeId() . '.' . $field_config->getTargetBundle());
+ \Drupal::service('plugin.manager.block')->clearCachedDefinitions();
}
diff --git a/core/modules/layout_builder/layout_builder.services.yml b/core/modules/layout_builder/layout_builder.services.yml
index db6a1c1..3893307 100644
--- a/core/modules/layout_builder/layout_builder.services.yml
+++ b/core/modules/layout_builder/layout_builder.services.yml
@@ -9,6 +9,8 @@ services:
layout_builder.routes:
class: Drupal\layout_builder\Routing\LayoutBuilderRoutes
arguments: ['@entity_type.manager', '@entity_field.manager']
+ tags:
+ - { name: event_subscriber }
layout_builder.route_enhancer:
class: Drupal\layout_builder\Routing\LayoutBuilderRouteEnhancer
tags:
@@ -18,6 +20,9 @@ services:
arguments: ['@layout_builder.tempstore_repository', '@class_resolver']
tags:
- { name: paramconverter, priority: 10 }
+ layout_builder.section_storage_param_converter.defaults:
+ class: Drupal\layout_builder\Routing\SectionStorageDefaultsParamConverter
+ arguments: ['@entity.manager']
layout_builder.section_storage_param_converter.overrides:
class: Drupal\layout_builder\Routing\SectionStorageOverridesParamConverter
arguments: ['@entity.manager']
diff --git a/core/modules/layout_builder/src/Cache/LayoutBuilderIsActiveCacheContext.php b/core/modules/layout_builder/src/Cache/LayoutBuilderIsActiveCacheContext.php
index c632f4b..3c3bc25 100644
--- a/core/modules/layout_builder/src/Cache/LayoutBuilderIsActiveCacheContext.php
+++ b/core/modules/layout_builder/src/Cache/LayoutBuilderIsActiveCacheContext.php
@@ -4,8 +4,8 @@ namespace Drupal\layout_builder\Cache;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Cache\Context\CalculatedCacheContextInterface;
-use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\layout_builder\OverridesSectionStorageInterface;
/**
* Determines whether Layout Builder is active for a given entity type or not.
@@ -49,7 +49,7 @@ class LayoutBuilderIsActiveCacheContext implements CalculatedCacheContextInterfa
}
$display = $this->getDisplay($entity_type_id);
- return ($display && $display->getThirdPartySetting('layout_builder', 'allow_custom', FALSE)) ? '1' : '0';
+ return ($display && $display->isOverridable()) ? '1' : '0';
}
/**
@@ -72,15 +72,15 @@ class LayoutBuilderIsActiveCacheContext implements CalculatedCacheContextInterfa
*
* @param string $entity_type_id
* The entity type ID.
- * @param string $view_mode
- * (optional) The view mode that should be used to render the entity.
*
- * @return \Drupal\Core\Entity\Display\EntityViewDisplayInterface|null
+ * @return \Drupal\layout_builder\Entity\LayoutEntityDisplayInterface|null
* The entity view display, if it exists.
*/
- protected function getDisplay($entity_type_id, $view_mode = 'full') {
+ protected function getDisplay($entity_type_id) {
if ($entity = $this->routeMatch->getParameter($entity_type_id)) {
- return EntityViewDisplay::collectRenderDisplay($entity, $view_mode);
+ if ($entity instanceof OverridesSectionStorageInterface) {
+ return $entity->getDefaultSectionStorage();
+ }
}
}
diff --git a/core/modules/layout_builder/src/Controller/LayoutBuilderController.php b/core/modules/layout_builder/src/Controller/LayoutBuilderController.php
index 5ccddfa..9642c4e 100644
--- a/core/modules/layout_builder/src/Controller/LayoutBuilderController.php
+++ b/core/modules/layout_builder/src/Controller/LayoutBuilderController.php
@@ -3,11 +3,13 @@
namespace Drupal\layout_builder\Controller;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Plugin\PluginFormInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Drupal\layout_builder\Context\LayoutBuilderContextTrait;
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
+use Drupal\layout_builder\OverridesSectionStorageInterface;
use Drupal\layout_builder\Section;
use Drupal\layout_builder\SectionStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -31,13 +33,23 @@ class LayoutBuilderController implements ContainerInjectionInterface {
protected $layoutTempstoreRepository;
/**
+ * The messenger service.
+ *
+ * @var \Drupal\Core\Messenger\MessengerInterface
+ */
+ protected $messenger;
+
+ /**
* LayoutBuilderController constructor.
*
* @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
* The layout tempstore repository.
+ * @param \Drupal\Core\Messenger\MessengerInterface $messenger
+ * The messenger service.
*/
- public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository) {
+ public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository, MessengerInterface $messenger) {
$this->layoutTempstoreRepository = $layout_tempstore_repository;
+ $this->messenger = $messenger;
}
/**
@@ -45,7 +57,8 @@ class LayoutBuilderController implements ContainerInjectionInterface {
*/
public static function create(ContainerInterface $container) {
return new static(
- $container->get('layout_builder.tempstore_repository')
+ $container->get('layout_builder.tempstore_repository'),
+ $container->get('messenger')
);
}
@@ -101,9 +114,16 @@ class LayoutBuilderController implements ContainerInjectionInterface {
* Indicates if the layout is rebuilding.
*/
protected function prepareLayout(SectionStorageInterface $section_storage, $is_rebuilding) {
- // For a new layout, begin with a single section of one column.
+ // Only add sections if the layout is new and empty.
if (!$is_rebuilding && $section_storage->count() === 0) {
$sections = [];
+ // If this is an empty override, copy the sections from the corresponding
+ // default.
+ if ($section_storage instanceof OverridesSectionStorageInterface) {
+ $sections = $section_storage->getDefaultSectionStorage()->getSections();
+ }
+
+ // For an empty layout, begin with a single section of one column.
if (!$sections) {
$sections[] = new Section('layout_onecol');
}
@@ -172,7 +192,7 @@ class LayoutBuilderController implements ContainerInjectionInterface {
$section = $section_storage->getSection($delta);
$layout = $section->getLayout();
- $build = $section->toRenderArray($this->getAvailableContexts($section_storage));
+ $build = $section->toRenderArray($this->getAvailableContexts($section_storage), TRUE);
$layout_definition = $layout->getPluginDefinition();
foreach ($layout_definition->getRegions() as $region => $info) {
@@ -277,6 +297,14 @@ class LayoutBuilderController implements ContainerInjectionInterface {
public function saveLayout(SectionStorageInterface $section_storage) {
$section_storage->save();
$this->layoutTempstoreRepository->delete($section_storage);
+
+ if ($section_storage instanceof OverridesSectionStorageInterface) {
+ $this->messenger->addMessage($this->t('The layout override has been saved.'));
+ }
+ else {
+ $this->messenger->addMessage($this->t('The layout has been saved.'));
+ }
+
return new RedirectResponse($section_storage->getCanonicalUrl()->setAbsolute()->toString());
}
@@ -291,6 +319,9 @@ class LayoutBuilderController implements ContainerInjectionInterface {
*/
public function cancelLayout(SectionStorageInterface $section_storage) {
$this->layoutTempstoreRepository->delete($section_storage);
+
+ $this->messenger->addMessage($this->t('The changes to the layout have been discarded.'));
+
return new RedirectResponse($section_storage->getCanonicalUrl()->setAbsolute()->toString());
}
diff --git a/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php
new file mode 100644
index 0000000..a612eb5
--- /dev/null
+++ b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php
@@ -0,0 +1,488 @@
+<?php
+
+namespace Drupal\layout_builder\Entity;
+
+use Drupal\Component\Plugin\DependentPluginInterface;
+use Drupal\Component\Plugin\PluginInspectionInterface;
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Entity\ContentEntityStorageInterface;
+use Drupal\Core\Entity\Entity\EntityViewDisplay as BaseEntityViewDisplay;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Entity\FieldableEntityInterface;
+use Drupal\Core\Plugin\Context\Context;
+use Drupal\Core\Plugin\Context\ContextDefinition;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Core\Url;
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\layout_builder\Section;
+use Drupal\layout_builder\SectionComponent;
+
+/**
+ * Provides an entity view display entity that has a layout.
+ *
+ * @internal
+ * Layout Builder is currently experimental and should only be leveraged by
+ * experimental modules and development releases of contributed modules.
+ * See https://www.drupal.org/core/experimental for more information.
+ */
+class LayoutBuilderEntityViewDisplay extends BaseEntityViewDisplay implements LayoutEntityDisplayInterface {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isOverridable() {
+ return $this->getThirdPartySetting('layout_builder', 'allow_custom', FALSE);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setOverridable($overridable = TRUE) {
+ $this->setThirdPartySetting('layout_builder', 'allow_custom', $overridable);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSections() {
+ return $this->getThirdPartySetting('layout_builder', 'sections', []);
+ }
+
+ /**
+ * Store the information for all sections.
+ *
+ * @param \Drupal\layout_builder\Section[] $sections
+ * The sections information.
+ *
+ * @return $this
+ */
+ protected function setSections(array $sections) {
+ $this->setThirdPartySetting('layout_builder', 'sections', array_values($sections));
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function count() {
+ return count($this->getSections());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSection($delta) {
+ if (!$this->hasSection($delta)) {
+ throw new \OutOfBoundsException(sprintf('Invalid delta "%s" for the "%s" entity', $delta, $this->id()));
+ }
+
+ return $this->getSections()[$delta];
+ }
+
+ /**
+ * Sets the section for the given delta on the display.
+ *
+ * @param int $delta
+ * The delta of the section.
+ * @param \Drupal\layout_builder\Section $section
+ * The layout section.
+ *
+ * @return $this
+ */
+ protected function setSection($delta, Section $section) {
+ $sections = $this->getSections();
+ $sections[$delta] = $section;
+ $this->setSections($sections);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function appendSection(Section $section) {
+ $delta = $this->count();
+
+ $this->setSection($delta, $section);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function insertSection($delta, Section $section) {
+ if ($this->hasSection($delta)) {
+ $sections = $this->getSections();
+ // @todo Use https://www.drupal.org/node/66183 once resolved.
+ $start = array_slice($sections, 0, $delta);
+ $end = array_slice($sections, $delta);
+ $this->setSections(array_merge($start, [$section], $end));
+ }
+ else {
+ $this->appendSection($section);
+ }
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function removeSection($delta) {
+ $sections = $this->getSections();
+ unset($sections[$delta]);
+ $this->setSections($sections);
+ return $this;
+ }
+
+ /**
+ * Indicates if there is a section at the specified delta.
+ *
+ * @param int $delta
+ * The delta of the section.
+ *
+ * @return bool
+ * TRUE if there is a section for this delta, FALSE otherwise.
+ */
+ protected function hasSection($delta) {
+ $sections = $this->getSections();
+ return isset($sections[$delta]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function preSave(EntityStorageInterface $storage) {
+ parent::preSave($storage);
+
+ $original_value = isset($this->original) ? $this->original->isOverridable() : FALSE;
+ $new_value = $this->isOverridable();
+ if ($original_value !== $new_value) {
+ $entity_type_id = $this->getTargetEntityTypeId();
+ $bundle = $this->getTargetBundle();
+
+ if ($new_value) {
+ $this->addSectionField($entity_type_id, $bundle, 'layout_builder__layout');
+ }
+ elseif ($field = FieldConfig::loadByName($entity_type_id, $bundle, 'layout_builder__layout')) {
+ $field->delete();
+ }
+ }
+ }
+
+ /**
+ * Adds a layout section field to a given bundle.
+ *
+ * @param string $entity_type_id
+ * The entity type ID.
+ * @param string $bundle
+ * The bundle.
+ * @param string $field_name
+ * The name for the layout section field.
+ */
+ protected function addSectionField($entity_type_id, $bundle, $field_name) {
+ $field = FieldConfig::loadByName($entity_type_id, $bundle, $field_name);
+ if (!$field) {
+ $field_storage = FieldStorageConfig::loadByName($entity_type_id, $field_name);
+ if (!$field_storage) {
+ $field_storage = FieldStorageConfig::create([
+ 'entity_type' => $entity_type_id,
+ 'field_name' => $field_name,
+ 'type' => 'layout_section',
+ 'locked' => TRUE,
+ ]);
+ $field_storage->save();
+ }
+
+ $field = FieldConfig::create([
+ 'field_storage' => $field_storage,
+ 'bundle' => $bundle,
+ 'label' => t('Layout'),
+ ]);
+ $field->save();
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getDefaultRegion() {
+ if ($this->hasSection(0)) {
+ return $this->getSection(0)->getDefaultRegion();
+ }
+
+ return parent::getDefaultRegion();
+ }
+
+ /**
+ * Wraps the context repository service.
+ *
+ * @return \Drupal\Core\Plugin\Context\ContextRepositoryInterface
+ * The context repository service.
+ */
+ protected function contextRepository() {
+ return \Drupal::service('context.repository');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildMultiple(array $entities) {
+ $build_list = parent::buildMultiple($entities);
+
+ foreach ($entities as $id => $entity) {
+ $sections = $this->getRuntimeSections($entity);
+ if ($sections) {
+ foreach ($build_list[$id] as $name => $build_part) {
+ $field_definition = $this->getFieldDefinition($name);
+ if ($field_definition && $field_definition->isDisplayConfigurable($this->displayContext)) {
+ unset($build_list[$id][$name]);
+ }
+ }
+
+ // Bypass ::getActiveContexts() in order to use the runtime entity, not
+ // a sample entity.
+ $contexts = $this->contextRepository()->getAvailableContexts();
+ // @todo Use EntityContextDefinition after resolving
+ // https://www.drupal.org/node/2932462.
+ $contexts['layout_builder.entity'] = new Context(new ContextDefinition("entity:{$entity->getEntityTypeId()}", new TranslatableMarkup('@entity being viewed', ['@entity' => $entity->getEntityType()->getLabel()])), $entity);
+ foreach ($sections as $delta => $section) {
+ $build_list[$id]['_layout_builder'][$delta] = $section->toRenderArray($contexts);
+ }
+ }
+ }
+
+ return $build_list;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getContexts() {
+ $entity = $this->getSampleEntity($this->getTargetEntityTypeId(), $this->getTargetBundle());
+ $context_label = new TranslatableMarkup('@entity being viewed', ['@entity' => $entity->getEntityType()->getLabel()]);
+
+ // @todo Use EntityContextDefinition after resolving
+ // https://www.drupal.org/node/2932462.
+ $contexts = [];
+ $contexts['layout_builder.entity'] = new Context(new ContextDefinition("entity:{$entity->getEntityTypeId()}", $context_label), $entity);
+ return $contexts;
+ }
+
+ /**
+ * Returns a sample entity.
+ *
+ * @param string $entity_type_id
+ * The entity type ID.
+ * @param string $bundle_id
+ * The bundle ID.
+ *
+ * @return \Drupal\Core\Entity\EntityInterface
+ * An entity.
+ */
+ protected function getSampleEntity($entity_type_id, $bundle_id) {
+ /** @var \Drupal\Core\TempStore\SharedTempStore $tempstore */
+ $tempstore = \Drupal::service('tempstore.shared')->get('layout_builder.sample_entity');
+ if ($entity = $tempstore->get("$entity_type_id.$bundle_id")) {
+ return $entity;
+ }
+
+ $entity_storage = $this->entityTypeManager()->getStorage($entity_type_id);
+ if (!$entity_storage instanceof ContentEntityStorageInterface) {
+ throw new \InvalidArgumentException(sprintf('The "%s" entity storage is not supported', $entity_type_id));
+ }
+
+ $entity = $entity_storage->createWithSampleValues($bundle_id);
+ // Mark the sample entity as being a preview.
+ $entity->in_preview = TRUE;
+ $tempstore->set("$entity_type_id.$bundle_id", $entity);
+ return $entity;
+ }
+
+ /**
+ * Gets the runtime sections for a given entity.
+ *
+ * @param \Drupal\Core\Entity\FieldableEntityInterface $entity
+ * The entity.
+ *
+ * @return \Drupal\layout_builder\Section[]
+ * The sections.
+ */
+ protected function getRuntimeSections(FieldableEntityInterface $entity) {
+ if ($this->isOverridable() && !$entity->get('layout_builder__layout')->isEmpty()) {
+ return $entity->get('layout_builder__layout')->getSections();
+ }
+
+ return $this->getSections();
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @todo Move this upstream in https://www.drupal.org/node/2939931.
+ */
+ public function label() {
+ $bundle_info = \Drupal::service('entity_type.bundle.info')->getBundleInfo($this->getTargetEntityTypeId());
+ $bundle_label = $bundle_info[$this->getTargetBundle()]['label'];
+ $target_entity_type = $this->entityTypeManager()->getDefinition($this->getTargetEntityTypeId());
+ return new TranslatableMarkup('@bundle @label', ['@bundle' => $bundle_label, '@label' => $target_entity_type->getPluralLabel()]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getStorageType() {
+ return 'defaults';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getStorageId() {
+ return $this->id();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCanonicalUrl() {
+ return Url::fromRoute("entity.entity_view_display.{$this->getTargetEntityTypeId()}.view_mode", $this->getRouteParameters());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getLayoutBuilderUrl() {
+ return Url::fromRoute("entity.entity_view_display.{$this->getTargetEntityTypeId()}.layout_builder", $this->getRouteParameters());
+ }
+
+ /**
+ * Returns the route parameters needed to build routes for this entity.
+ *
+ * @return string[]
+ * An array of route parameters.
+ */
+ protected function getRouteParameters() {
+ $route_parameters = [];
+
+ $entity_type = $this->entityTypeManager()->getDefinition($this->getTargetEntityTypeId());
+ $bundle_parameter_key = $entity_type->getBundleEntityType() ?: 'bundle';
+ $route_parameters[$bundle_parameter_key] = $this->getTargetBundle();
+
+ $route_parameters['view_mode_name'] = $this->getMode();
+ return $route_parameters;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function calculateDependencies() {
+ parent::calculateDependencies();
+
+ foreach ($this->getSections() as $delta => $section) {
+ foreach ($section->getComponents() as $uuid => $component) {
+ $this->calculatePluginDependencies($component->getPlugin());
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function onDependencyRemoval(array $dependencies) {
+ $changed = parent::onDependencyRemoval($dependencies);
+
+ // Loop through all components and determine if the removed dependencies are
+ // used by their plugins.
+ foreach ($this->getSections() as $delta => $section) {
+ foreach ($section->getComponents() as $uuid => $component) {
+ $plugin_dependencies = $this->getPluginDependencies($component->getPlugin());
+ $component_removed_dependencies = $this->getPluginRemovedDependencies($plugin_dependencies, $dependencies);
+ if ($component_removed_dependencies) {
+ // @todo Allow the plugins to react to their dependency removal in
+ // https://www.drupal.org/project/drupal/issues/2579743.
+ $section->removeComponent($uuid);
+ $changed = TRUE;
+ }
+ }
+ }
+ return $changed;
+ }
+
+ /**
+ * Calculates and returns dependencies of a specific plugin instance.
+ *
+ * @param \Drupal\Component\Plugin\PluginInspectionInterface $instance
+ * The plugin instance.
+ *
+ * @return array
+ * An array of dependencies keyed by the type of dependency.
+ *
+ * @todo Replace this in https://www.drupal.org/project/drupal/issues/2939925.
+ */
+ protected function getPluginDependencies(PluginInspectionInterface $instance) {
+ $definition = $instance->getPluginDefinition();
+ $dependencies['module'][] = $definition['provider'];
+ // Plugins can declare additional dependencies in their definition.
+ if (isset($definition['config_dependencies'])) {
+ $dependencies = NestedArray::mergeDeep($dependencies, $definition['config_dependencies']);
+ }
+
+ // If a plugin is dependent, calculate its dependencies.
+ if ($instance instanceof DependentPluginInterface && $plugin_dependencies = $instance->calculateDependencies()) {
+ $dependencies = NestedArray::mergeDeep($dependencies, $plugin_dependencies);
+ }
+ return $dependencies;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setComponent($name, array $options = []) {
+ parent::setComponent($name, $options);
+
+ // @todo Remove workaround for EntityViewBuilder::getSingleFieldDisplay() in
+ // https://www.drupal.org/project/drupal/issues/2936464.
+ if ($this->getMode() === static::CUSTOM_MODE) {
+ return $this;
+ }
+
+ // Retrieve the updated options after the parent:: call.
+ $options = $this->content[$name];
+ // Provide backwards compatibility by converting to a section component.
+ $field_definition = $this->getFieldDefinition($name);
+ if ($field_definition && $field_definition->isDisplayConfigurable('view') && isset($options['type'])) {
+ $configuration = [];
+ $configuration['id'] = 'field_block:' . $this->getTargetEntityTypeId() . ':' . $this->getTargetBundle() . ':' . $name;
+ $configuration['label_display'] = FALSE;
+ $keys = array_flip(['type', 'label', 'settings', 'third_party_settings']);
+ $configuration['formatter'] = array_intersect_key($options, $keys);
+ $configuration['context_mapping']['entity'] = 'layout_builder.entity';
+
+ $section = $this->getDefaultSection();
+ $region = isset($options['region']) ? $options['region'] : $section->getDefaultRegion();
+ $new_component = (new SectionComponent(\Drupal::service('uuid')->generate(), $region, $configuration));
+ $section->appendComponent($new_component);
+ }
+ return $this;
+ }
+
+ /**
+ * Gets a default section.
+ *
+ * @return \Drupal\layout_builder\Section
+ * The default section.
+ */
+ protected function getDefaultSection() {
+ // If no section exists, append a new one.
+ if (!$this->hasSection(0)) {
+ $this->appendSection(new Section('layout_onecol'));
+ }
+
+ // Return the first section.
+ return $this->getSection(0);
+ }
+
+}
diff --git a/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplayStorage.php b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplayStorage.php
new file mode 100644
index 0000000..e86df9d
--- /dev/null
+++ b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplayStorage.php
@@ -0,0 +1,60 @@
+<?php
+
+namespace Drupal\layout_builder\Entity;
+
+use Drupal\Core\Config\Entity\ConfigEntityStorage;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\layout_builder\Section;
+use Drupal\layout_builder\SectionComponent;
+
+/**
+ * Provides storage for entity view display entities that have layouts.
+ *
+ * @internal
+ * Layout Builder is currently experimental and should only be leveraged by
+ * experimental modules and development releases of contributed modules.
+ * See https://www.drupal.org/core/experimental for more information.
+ */
+class LayoutBuilderEntityViewDisplayStorage extends ConfigEntityStorage {
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function mapToStorageRecord(EntityInterface $entity) {
+ $record = parent::mapToStorageRecord($entity);
+
+ if (!empty($record['third_party_settings']['layout_builder']['sections'])) {
+ $record['third_party_settings']['layout_builder']['sections'] = array_map(function (Section $section) {
+ return $section->toArray();
+ }, $record['third_party_settings']['layout_builder']['sections']);
+ }
+ return $record;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function mapFromStorageRecords(array $records) {
+ foreach ($records as $id => &$record) {
+ if (!empty($record['third_party_settings']['layout_builder']['sections'])) {
+ $sections = &$record['third_party_settings']['layout_builder']['sections'];
+ foreach ($sections as $section_delta => $section) {
+ $sections[$section_delta] = new Section(
+ $section['layout_id'],
+ $section['layout_settings'],
+ array_map(function (array $component) {
+ return (new SectionComponent(
+ $component['uuid'],
+ $component['region'],
+ $component['configuration'],
+ $component['additional']
+ ))->setWeight($component['weight']);
+ }, $section['components'])
+ );
+ }
+ }
+ }
+ return parent::mapFromStorageRecords($records);
+ }
+
+}
diff --git a/core/modules/layout_builder/src/Entity/LayoutEntityDisplayInterface.php b/core/modules/layout_builder/src/Entity/LayoutEntityDisplayInterface.php
new file mode 100644
index 0000000..1affc53
--- /dev/null
+++ b/core/modules/layout_builder/src/Entity/LayoutEntityDisplayInterface.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace Drupal\layout_builder\Entity;
+
+use Drupal\Core\Entity\Display\EntityDisplayInterface;
+use Drupal\layout_builder\SectionStorageInterface;
+
+/**
+ * Provides an interface for entity displays that have layout.
+ *
+ * @internal
+ * Layout Builder is currently experimental and should only be leveraged by
+ * experimental modules and development releases of contributed modules.
+ * See https://www.drupal.org/core/experimental for more information.
+ */
+interface LayoutEntityDisplayInterface extends EntityDisplayInterface, SectionStorageInterface {
+
+ /**
+ * Determines if the display allows custom overrides.
+ *
+ * @return bool
+ * TRUE if custom overrides are allowed, FALSE otherwise.
+ */
+ public function isOverridable();
+
+ /**
+ * Sets the display to allow or disallow overrides.
+ *
+ * @param bool $overridable
+ * TRUE if the display should allow overrides, FALSE otherwise.
+ *
+ * @return $this
+ */
+ public function setOverridable($overridable = TRUE);
+
+}
diff --git a/core/modules/layout_builder/src/Field/LayoutSectionItemList.php b/core/modules/layout_builder/src/Field/LayoutSectionItemList.php
index 5aabb02..f67f90e 100644
--- a/core/modules/layout_builder/src/Field/LayoutSectionItemList.php
+++ b/core/modules/layout_builder/src/Field/LayoutSectionItemList.php
@@ -6,6 +6,8 @@ use Drupal\Core\Field\FieldItemList;
use Drupal\Core\Plugin\Context\Context;
use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
+use Drupal\layout_builder\OverridesSectionStorageInterface;
use Drupal\layout_builder\Section;
use Drupal\layout_builder\SectionStorageInterface;
@@ -16,7 +18,7 @@ use Drupal\layout_builder\SectionStorageInterface;
*
* @see \Drupal\layout_builder\Plugin\Field\FieldType\LayoutSectionItem
*/
-class LayoutSectionItemList extends FieldItemList implements SectionStorageInterface {
+class LayoutSectionItemList extends FieldItemList implements SectionStorageInterface, OverridesSectionStorageInterface {
/**
* {@inheritdoc}
@@ -27,6 +29,7 @@ class LayoutSectionItemList extends FieldItemList implements SectionStorageInter
$item = $this->createItem($delta);
$item->section = $section;
+ // @todo Use https://www.drupal.org/node/66183 once resolved.
$start = array_slice($this->list, 0, $delta);
$end = array_slice($this->list, $delta);
$this->list = array_merge($start, [$item], $end);
@@ -91,7 +94,7 @@ class LayoutSectionItemList extends FieldItemList implements SectionStorageInter
/**
* {@inheritdoc}
*/
- public function getStorageType() {
+ public static function getStorageType() {
return 'overrides';
}
@@ -134,6 +137,13 @@ class LayoutSectionItemList extends FieldItemList implements SectionStorageInter
/**
* {@inheritdoc}
*/
+ public function getDefaultSectionStorage() {
+ return LayoutBuilderEntityViewDisplay::collectRenderDisplay($this->getEntity(), 'default');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
public function __wakeup() {
// Ensure the entity is updated with the latest value.
$this->getEntity()->set($this->getName(), $this->getValue());
diff --git a/core/modules/layout_builder/src/Form/AddBlockForm.php b/core/modules/layout_builder/src/Form/AddBlockForm.php
index 83effd6..704d136 100644
--- a/core/modules/layout_builder/src/Form/AddBlockForm.php
+++ b/core/modules/layout_builder/src/Form/AddBlockForm.php
@@ -2,8 +2,9 @@
namespace Drupal\layout_builder\Form;
-use Drupal\layout_builder\Section;
+use Drupal\Core\Form\FormStateInterface;
use Drupal\layout_builder\SectionComponent;
+use Drupal\layout_builder\SectionStorageInterface;
/**
* Provides a form to add a block.
@@ -27,10 +28,32 @@ class AddBlockForm extends ConfigureBlockFormBase {
}
/**
- * {@inheritdoc}
+ * Builds the form for the block.
+ *
+ * @param array $form
+ * An associative array containing the structure of the form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ * @param \Drupal\layout_builder\SectionStorageInterface $section_storage
+ * The section storage being configured.
+ * @param int $delta
+ * The delta of the section.
+ * @param string $region
+ * The region of the block.
+ * @param string|null $plugin_id
+ * The plugin ID of the block to add.
+ *
+ * @return array
+ * The form array.
*/
- protected function submitBlock(Section $section, $region, $uuid, array $configuration) {
- $section->appendComponent(new SectionComponent($uuid, $region, $configuration));
+ public function buildForm(array $form, FormStateInterface $form_state, SectionStorageInterface $section_storage = NULL, $delta = NULL, $region = NULL, $plugin_id = NULL) {
+ // Only generate a new component once per form submission.
+ if (!$component = $form_state->getTemporaryValue('layout_builder__component')) {
+ $component = new SectionComponent($this->uuidGenerator->generate(), $region, ['id' => $plugin_id]);
+ $section_storage->getSection($delta)->appendComponent($component);
+ $form_state->setTemporaryValue('layout_builder__component', $component);
+ }
+ return $this->doBuildForm($form, $form_state, $section_storage, $delta, $component);
}
}
diff --git a/core/modules/layout_builder/src/Form/ConfigureBlockFormBase.php b/core/modules/layout_builder/src/Form/ConfigureBlockFormBase.php
index b75e88f..e1103e9 100644
--- a/core/modules/layout_builder/src/Form/ConfigureBlockFormBase.php
+++ b/core/modules/layout_builder/src/Form/ConfigureBlockFormBase.php
@@ -17,7 +17,7 @@ use Drupal\Core\Plugin\PluginWithFormsInterface;
use Drupal\layout_builder\Context\LayoutBuilderContextTrait;
use Drupal\layout_builder\Controller\LayoutRebuildTrait;
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
-use Drupal\layout_builder\Section;
+use Drupal\layout_builder\SectionComponent;
use Drupal\layout_builder\SectionStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -59,7 +59,7 @@ abstract class ConfigureBlockFormBase extends FormBase {
*
* @var \Drupal\Component\Uuid\UuidInterface
*/
- protected $uuid;
+ protected $uuidGenerator;
/**
* The plugin form manager.
@@ -83,6 +83,13 @@ abstract class ConfigureBlockFormBase extends FormBase {
protected $region;
/**
+ * The UUID of the component.
+ *
+ * @var string
+ */
+ protected $uuid;
+
+ /**
* The section storage.
*
* @var \Drupal\layout_builder\SectionStorageInterface
@@ -109,7 +116,7 @@ abstract class ConfigureBlockFormBase extends FormBase {
$this->layoutTempstoreRepository = $layout_tempstore_repository;
$this->contextRepository = $context_repository;
$this->blockManager = $block_manager;
- $this->uuid = $uuid;
+ $this->uuidGenerator = $uuid;
$this->classResolver = $class_resolver;
$this->pluginFormFactory = $plugin_form_manager;
}
@@ -129,25 +136,6 @@ abstract class ConfigureBlockFormBase extends FormBase {
}
/**
- * Prepares the block plugin based on the block ID.
- *
- * @param string $block_id
- * Either a block ID, or the plugin ID used to create a new block.
- * @param array $configuration
- * The block configuration.
- *
- * @return \Drupal\Core\Block\BlockPluginInterface
- * The block plugin.
- */
- protected function prepareBlock($block_id, array $configuration) {
- if (!isset($configuration['uuid'])) {
- $configuration['uuid'] = $this->uuid->generate();
- }
-
- return $this->blockManager->createInstance($block_id, $configuration);
- }
-
- /**
* Builds the form for the block.
*
* @param array $form
@@ -158,21 +146,17 @@ abstract class ConfigureBlockFormBase extends FormBase {
* The section storage being configured.
* @param int $delta
* The delta of the section.
- * @param string $region
- * The region of the block.
- * @param string|null $plugin_id
- * The plugin ID of the block to add.
- * @param array $configuration
- * (optional) The array of configuration for the block.
+ * @param \Drupal\layout_builder\SectionComponent $component
+ * The section component containing the block.
*
* @return array
* The form array.
*/
- public function buildForm(array $form, FormStateInterface $form_state, SectionStorageInterface $section_storage = NULL, $delta = NULL, $region = NULL, $plugin_id = NULL, array $configuration = []) {
+ public function doBuildForm(array $form, FormStateInterface $form_state, SectionStorageInterface $section_storage = NULL, $delta = NULL, SectionComponent $component = NULL) {
$this->sectionStorage = $section_storage;
$this->delta = $delta;
- $this->region = $region;
- $this->block = $this->prepareBlock($plugin_id, $configuration);
+ $this->uuid = $component->getUuid();
+ $this->block = $component->getPlugin();
$form_state->setTemporaryValue('gathered_contexts', $this->getAvailableContexts($section_storage));
@@ -205,20 +189,6 @@ abstract class ConfigureBlockFormBase extends FormBase {
abstract protected function submitLabel();
/**
- * Handles the submission of a block.
- *
- * @param \Drupal\layout_builder\Section $section
- * The layout section.
- * @param string $region
- * The region name.
- * @param string $uuid
- * The UUID of the block.
- * @param array $configuration
- * The block configuration.
- */
- abstract protected function submitBlock(Section $section, $region, $uuid, array $configuration);
-
- /**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
@@ -242,7 +212,7 @@ abstract class ConfigureBlockFormBase extends FormBase {
$configuration = $this->block->getConfiguration();
$section = $this->sectionStorage->getSection($this->delta);
- $this->submitBlock($section, $this->region, $configuration['uuid'], $configuration);
+ $section->getComponent($this->uuid)->setConfiguration($configuration);
$this->layoutTempstoreRepository->set($this->sectionStorage);
$form_state->setRedirectUrl($this->sectionStorage->getLayoutBuilderUrl());
diff --git a/core/modules/layout_builder/src/Form/LayoutBuilderEntityViewDisplayForm.php b/core/modules/layout_builder/src/Form/LayoutBuilderEntityViewDisplayForm.php
new file mode 100644
index 0000000..9fa2064
--- /dev/null
+++ b/core/modules/layout_builder/src/Form/LayoutBuilderEntityViewDisplayForm.php
@@ -0,0 +1,94 @@
+<?php
+
+namespace Drupal\layout_builder\Form;
+
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\field_ui\Form\EntityViewDisplayEditForm;
+use Drupal\layout_builder\Entity\LayoutEntityDisplayInterface;
+
+/**
+ * Edit form for the LayoutBuilderEntityViewDisplay entity type.
+ *
+ * @internal
+ * Layout Builder is currently experimental and should only be leveraged by
+ * experimental modules and development releases of contributed modules.
+ * See https://www.drupal.org/core/experimental for more information.
+ */
+class LayoutBuilderEntityViewDisplayForm extends EntityViewDisplayEditForm {
+
+ /**
+ * The entity being used by this form.
+ *
+ * @var \Drupal\layout_builder\Entity\LayoutEntityDisplayInterface
+ */
+ protected $entity;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function form(array $form, FormStateInterface $form_state) {
+ $form = parent::form($form, $form_state);
+
+ // Hide the table of fields.
+ $form['fields']['#access'] = FALSE;
+ $form['#fields'] = [];
+ $form['#extra'] = [];
+
+ $form['manage_layout'] = [
+ '#type' => 'link',
+ '#title' => $this->t('Manage layout'),
+ '#weight' => -10,
+ '#attributes' => ['class' => ['button']],
+ '#url' => $this->entity->getLayoutBuilderUrl(),
+ ];
+
+ // @todo Expand to work for all view modes in
+ // https://www.drupal.org/node/2907413.
+ if ($this->entity->getMode() === 'default') {
+ $form['layout'] = [
+ '#type' => 'details',
+ '#open' => TRUE,
+ '#title' => $this->t('Layout options'),
+ '#tree' => TRUE,
+ ];
+
+ $entity_type = $this->entityTypeManager->getDefinition($this->entity->getTargetEntityTypeId());
+ // @todo Unchecking this box is a destructive action, this should be made
+ // clear to the user in https://www.drupal.org/node/2914484.
+ $form['layout']['allow_custom'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Allow each @entity to have its layout customized.', [
+ '@entity' => $entity_type->getSingularLabel(),
+ ]),
+ '#default_value' => $this->entity->isOverridable(),
+ ];
+
+ $form['#entity_builders'][] = '::entityFormEntityBuild';
+ }
+ return $form;
+ }
+
+ /**
+ * Entity builder for layout options on the entity view display form.
+ */
+ public function entityFormEntityBuild($entity_type_id, LayoutEntityDisplayInterface $display, &$form, FormStateInterface &$form_state) {
+ $new_value = (bool) $form_state->getValue(['layout', 'allow_custom'], FALSE);
+ $display->setOverridable($new_value);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function buildFieldRow(FieldDefinitionInterface $field_definition, array $form, FormStateInterface $form_state) {
+ // Intentionally empty.
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function buildExtraFieldRow($field_id, $extra_field) {
+ // Intentionally empty.
+ }
+
+}
diff --git a/core/modules/layout_builder/src/Form/RevertOverridesForm.php b/core/modules/layout_builder/src/Form/RevertOverridesForm.php
new file mode 100644
index 0000000..b6d07d9
--- /dev/null
+++ b/core/modules/layout_builder/src/Form/RevertOverridesForm.php
@@ -0,0 +1,117 @@
+<?php
+
+namespace Drupal\layout_builder\Form;
+
+use Drupal\Core\Form\ConfirmFormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Messenger\MessengerInterface;
+use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
+use Drupal\layout_builder\OverridesSectionStorageInterface;
+use Drupal\layout_builder\SectionStorageInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Reverts the overridden layout to the defaults.
+ */
+class RevertOverridesForm extends ConfirmFormBase {
+
+ /**
+ * The layout tempstore repository.
+ *
+ * @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
+ */
+ protected $layoutTempstoreRepository;
+
+ /**
+ * The messenger service.
+ *
+ * @var \Drupal\Core\Messenger\MessengerInterface
+ */
+ protected $messenger;
+
+ /**
+ * The section storage.
+ *
+ * @var \Drupal\layout_builder\SectionStorageInterface
+ */
+ protected $sectionStorage;
+
+ /**
+ * Constructs a new RevertOverridesForm.
+ *
+ * @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
+ * The layout tempstore repository.
+ * @param \Drupal\Core\Messenger\MessengerInterface $messenger
+ * The messenger service.
+ */
+ public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository, MessengerInterface $messenger) {
+ $this->layoutTempstoreRepository = $layout_tempstore_repository;
+ $this->messenger = $messenger;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('layout_builder.tempstore_repository'),
+ $container->get('messenger')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormId() {
+ return 'layout_builder_revert_overrides';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getQuestion() {
+ return $this->t('Are you sure you want to revert this to defaults?');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getConfirmText() {
+ return $this->t('Revert');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCancelUrl() {
+ return $this->sectionStorage->getLayoutBuilderUrl();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(array $form, FormStateInterface $form_state, SectionStorageInterface $section_storage = NULL) {
+ if (!$section_storage instanceof OverridesSectionStorageInterface) {
+ throw new \InvalidArgumentException(sprintf('The section storage with type "%s" and ID "%s" does not provide overrides', $section_storage->getStorageType(), $section_storage->getStorageId()));
+ }
+
+ $this->sectionStorage = $section_storage;
+ return parent::buildForm($form, $form_state);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ // Remove all sections.
+ while ($this->sectionStorage->count()) {
+ $this->sectionStorage->removeSection(0);
+ }
+ $this->sectionStorage->save();
+ $this->layoutTempstoreRepository->delete($this->sectionStorage);
+
+ $this->messenger->addMessage($this->t('The layout has been reverted back to defaults.'));
+ $form_state->setRedirectUrl($this->getCancelUrl());
+ }
+
+}
diff --git a/core/modules/layout_builder/src/Form/UpdateBlockForm.php b/core/modules/layout_builder/src/Form/UpdateBlockForm.php
index afca0d2..c00b406 100644
--- a/core/modules/layout_builder/src/Form/UpdateBlockForm.php
+++ b/core/modules/layout_builder/src/Form/UpdateBlockForm.php
@@ -2,9 +2,7 @@
namespace Drupal\layout_builder\Form;
-use Drupal\Component\Plugin\ConfigurablePluginInterface;
use Drupal\Core\Form\FormStateInterface;
-use Drupal\layout_builder\Section;
use Drupal\layout_builder\SectionStorageInterface;
/**
@@ -36,19 +34,13 @@ class UpdateBlockForm extends ConfigureBlockFormBase {
* The region of the block.
* @param string $uuid
* The UUID of the block being updated.
- * @param array $configuration
- * (optional) The array of configuration for the block.
*
* @return array
* The form array.
*/
- public function buildForm(array $form, FormStateInterface $form_state, SectionStorageInterface $section_storage = NULL, $delta = NULL, $region = NULL, $uuid = NULL, array $configuration = []) {
- $plugin = $section_storage->getSection($delta)->getComponent($uuid)->getPlugin();
- if ($plugin instanceof ConfigurablePluginInterface) {
- $configuration = $plugin->getConfiguration();
- }
-
- return parent::buildForm($form, $form_state, $section_storage, $delta, $region, $plugin->getPluginId(), $configuration);
+ public function buildForm(array $form, FormStateInterface $form_state, SectionStorageInterface $section_storage = NULL, $delta = NULL, $region = NULL, $uuid = NULL) {
+ $component = $section_storage->getSection($delta)->getComponent($uuid);
+ return $this->doBuildForm($form, $form_state, $section_storage, $delta, $component);
}
/**
@@ -58,11 +50,4 @@ class UpdateBlockForm extends ConfigureBlockFormBase {
return $this->t('Update');
}
- /**
- * {@inheritdoc}
- */
- protected function submitBlock(Section $section, $region, $uuid, array $configuration) {
- $section->getComponent($uuid)->setConfiguration($configuration);
- }
-
}
diff --git a/core/modules/layout_builder/src/LayoutTempstoreRepository.php b/core/modules/layout_builder/src/LayoutTempstoreRepository.php
index 0fa9d73..39725af 100644
--- a/core/modules/layout_builder/src/LayoutTempstoreRepository.php
+++ b/core/modules/layout_builder/src/LayoutTempstoreRepository.php
@@ -71,7 +71,7 @@ class LayoutTempstoreRepository implements LayoutTempstoreRepositoryInterface {
* The tempstore.
*/
protected function getTempstore(SectionStorageInterface $section_storage) {
- $collection = 'layout_builder.' . $section_storage->getStorageType();
+ $collection = 'layout_builder.section_storage.' . $section_storage->getStorageType();
return $this->tempStoreFactory->get($collection);
}
diff --git a/core/modules/layout_builder/src/OverridesSectionStorageInterface.php b/core/modules/layout_builder/src/OverridesSectionStorageInterface.php
new file mode 100644
index 0000000..0d6fd2b
--- /dev/null
+++ b/core/modules/layout_builder/src/OverridesSectionStorageInterface.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Drupal\layout_builder;
+
+/**
+ * Defines an interface for an object that stores layout sections for overrides.
+ *
+ * @internal
+ * Layout Builder is currently experimental and should only be leveraged by
+ * experimental modules and development releases of contributed modules.
+ * See https://www.drupal.org/core/experimental for more information.
+ */
+interface OverridesSectionStorageInterface {
+
+ /**
+ * Returns the corresponding defaults section storage for this override.
+ *
+ * @return \Drupal\layout_builder\SectionStorageInterface
+ * The defaults section storage.
+ *
+ * @todo Determine if this method needs a parameter in
+ * https://www.drupal.org/project/drupal/issues/2936507.
+ */
+ public function getDefaultSectionStorage();
+
+}
diff --git a/core/modules/layout_builder/src/Plugin/Block/FieldBlock.php b/core/modules/layout_builder/src/Plugin/Block/FieldBlock.php
index 11b3e45..f011e32 100644
--- a/core/modules/layout_builder/src/Plugin/Block/FieldBlock.php
+++ b/core/modules/layout_builder/src/Plugin/Block/FieldBlock.php
@@ -17,7 +17,9 @@ use Drupal\Core\Field\FormatterPluginManager;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\ContextAwarePluginInterface;
+use Drupal\Core\Render\Element;
use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
@@ -52,6 +54,13 @@ class FieldBlock extends BlockBase implements ContextAwarePluginInterface, Conta
protected $entityTypeId;
/**
+ * The bundle ID.
+ *
+ * @var string
+ */
+ protected $bundle;
+
+ /**
* The field name.
*
* @var string
@@ -94,8 +103,9 @@ class FieldBlock extends BlockBase implements ContextAwarePluginInterface, Conta
$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);
+ list (, $entity_type_id, $bundle, $field_name) = explode(static::DERIVATIVE_SEPARATOR, $plugin_id, 4);
$this->entityTypeId = $entity_type_id;
+ $this->bundle = $bundle;
$this->fieldName = $field_name;
parent::__construct($configuration, $plugin_id, $plugin_definition);
@@ -130,7 +140,11 @@ class FieldBlock extends BlockBase implements ContextAwarePluginInterface, Conta
*/
public function build() {
$display_settings = $this->getConfiguration()['formatter'];
- $build = $this->getEntity()->get($this->fieldName)->view($display_settings);
+ $entity = $this->getEntity();
+ $build = $entity->get($this->fieldName)->view($display_settings);
+ if (!empty($entity->in_preview) && !Element::getVisibleChildren($build)) {
+ $build['content']['#markup'] = new TranslatableMarkup('Placeholder for the "@field" field', ['@field' => $this->getFieldDefinition()->getLabel()]);
+ }
CacheableMetadata::createFromObject($this)->applyTo($build);
return $build;
}
@@ -299,8 +313,7 @@ class FieldBlock extends BlockBase implements ContextAwarePluginInterface, Conta
*/
protected function getFieldDefinition() {
if (empty($this->fieldDefinition)) {
- $bundle = reset($this->getPluginDefinition()['bundles']);
- $field_definitions = $this->entityFieldManager->getFieldDefinitions($this->entityTypeId, $bundle);
+ $field_definitions = $this->entityFieldManager->getFieldDefinitions($this->entityTypeId, $this->bundle);
$this->fieldDefinition = $field_definitions[$this->fieldName];
}
return $this->fieldDefinition;
diff --git a/core/modules/layout_builder/src/Plugin/Derivative/FieldBlockDeriver.php b/core/modules/layout_builder/src/Plugin/Derivative/FieldBlockDeriver.php
index 6a39f4a..71b9c1e 100644
--- a/core/modules/layout_builder/src/Plugin/Derivative/FieldBlockDeriver.php
+++ b/core/modules/layout_builder/src/Plugin/Derivative/FieldBlockDeriver.php
@@ -6,6 +6,7 @@ use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Component\Plugin\PluginBase;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeRepositoryInterface;
+use Drupal\Core\Field\FieldConfigInterface;
use Drupal\Core\Field\FieldTypePluginManagerInterface;
use Drupal\Core\Field\FormatterPluginManager;
use Drupal\Core\Plugin\Context\ContextDefinition;
@@ -87,80 +88,47 @@ class FieldBlockDeriver extends DeriverBase implements ContainerDeriverInterface
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];
-
+ foreach ($entity_field_map as $field_name => $field_info) {
// Skip fields without any formatters.
- $options = $this->formatterManager->getOptions($field_storage_definition->getType());
+ $options = $this->formatterManager->getOptions($field_info['type']);
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'];
- }
+ foreach ($field_info['bundles'] as $bundle) {
+ $derivative = $base_plugin_definition;
+ $field_definition = $this->entityFieldManager->getFieldDefinitions($entity_type_id, $bundle)[$field_name];
- // 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;
+ // Store the default formatter on the definition.
+ $derivative['default_formatter'] = '';
+ $field_type_definition = $this->fieldTypeManager->getDefinition($field_info['type']);
+ if (isset($field_type_definition['default_formatter'])) {
+ $derivative['default_formatter'] = $field_type_definition['default_formatter'];
}
- $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']);
+ $derivative['category'] = $this->t('@entity', ['@entity' => $entity_type_labels[$entity_type_id]]);
- // 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['admin_label'] = $field_definition->getLabel();
+
+ // Add a dependency on the field if it is configurable.
+ if ($field_definition instanceof FieldConfigInterface) {
+ $derivative['config_dependencies'][$field_definition->getConfigDependencyKey()][] = $field_definition->getConfigDependencyName();
}
+ // For any field that is not display configurable, mark it as
+ // unavailable to place in the block UI.
+ $derivative['_block_ui_hidden'] = !$field_definition->isDisplayConfigurable('view');
+
+ // @todo Use EntityContextDefinition after resolving
+ // https://www.drupal.org/node/2932462.
+ $context_definition = new ContextDefinition('entity:' . $entity_type_id, $entity_type_labels[$entity_type_id], TRUE);
+ $context_definition->addConstraint('Bundle', [$bundle]);
+ $derivative['context'] = [
+ 'entity' => $context_definition,
+ ];
+
+ $derivative_id = $entity_type_id . PluginBase::DERIVATIVE_SEPARATOR . $bundle . PluginBase::DERIVATIVE_SEPARATOR . $field_name;
+ $this->derivatives[$derivative_id] = $derivative;
}
- $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/src/Plugin/Derivative/LayoutBuilderLocalTaskDeriver.php b/core/modules/layout_builder/src/Plugin/Derivative/LayoutBuilderLocalTaskDeriver.php
index 0aacc0d..94160a7 100644
--- a/core/modules/layout_builder/src/Plugin/Derivative/LayoutBuilderLocalTaskDeriver.php
+++ b/core/modules/layout_builder/src/Plugin/Derivative/LayoutBuilderLocalTaskDeriver.php
@@ -12,6 +12,8 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides local task definitions for the layout builder user interface.
*
+ * @todo Remove this in https://www.drupal.org/project/drupal/issues/2936655.
+ *
* @internal
*/
class LayoutBuilderLocalTaskDeriver extends DeriverBase implements ContainerDeriverInterface {
@@ -48,7 +50,8 @@ class LayoutBuilderLocalTaskDeriver extends DeriverBase implements ContainerDeri
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
- foreach (array_keys($this->getEntityTypes()) as $entity_type_id) {
+ foreach ($this->getEntityTypes() as $entity_type_id => $entity_type) {
+ // Overrides.
$this->derivatives["entity.$entity_type_id.layout_builder"] = $base_plugin_definition + [
'route_name' => "entity.$entity_type_id.layout_builder",
'weight' => 15,
@@ -72,6 +75,37 @@ class LayoutBuilderLocalTaskDeriver extends DeriverBase implements ContainerDeri
'weight' => 5,
'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id],
];
+ // @todo This link should be conditionally displayed, see
+ // https://www.drupal.org/node/2917777.
+ $this->derivatives["entity.$entity_type_id.layout_builder_revert"] = $base_plugin_definition + [
+ 'route_name' => "entity.$entity_type_id.layout_builder_revert",
+ 'title' => $this->t('Revert to defaults'),
+ 'parent_id' => "layout_builder_ui:entity.$entity_type_id.layout_builder",
+ 'entity_type_id' => $entity_type_id,
+ 'weight' => 10,
+ 'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id],
+ ];
+
+ // Defaults.
+ $this->derivatives["entity.entity_view_display.$entity_type_id.layout_builder"] = $base_plugin_definition + [
+ 'route_name' => "entity.entity_view_display.$entity_type_id.layout_builder",
+ 'title' => $this->t('Manage layout'),
+ 'base_route' => "entity.entity_view_display.$entity_type_id.layout_builder",
+ 'entity_type_id' => $entity_type_id,
+ ];
+ $this->derivatives["entity.entity_view_display.$entity_type_id.layout_builder_save"] = $base_plugin_definition + [
+ 'route_name' => "entity.entity_view_display.$entity_type_id.layout_builder_save",
+ 'title' => $this->t('Save Layout'),
+ 'parent_id' => "layout_builder_ui:entity.entity_view_display.$entity_type_id.layout_builder",
+ 'entity_type_id' => $entity_type_id,
+ ];
+ $this->derivatives["entity.entity_view_display.$entity_type_id.layout_builder_cancel"] = $base_plugin_definition + [
+ 'route_name' => "entity.entity_view_display.$entity_type_id.layout_builder_cancel",
+ 'title' => $this->t('Cancel Layout'),
+ 'weight' => 5,
+ 'parent_id' => "layout_builder_ui:entity.entity_view_display.$entity_type_id.layout_builder",
+ 'entity_type_id' => $entity_type_id,
+ ];
}
return $this->derivatives;
diff --git a/core/modules/layout_builder/src/Routing/LayoutBuilderRoutes.php b/core/modules/layout_builder/src/Routing/LayoutBuilderRoutes.php
index 563021e..500a380 100644
--- a/core/modules/layout_builder/src/Routing/LayoutBuilderRoutes.php
+++ b/core/modules/layout_builder/src/Routing/LayoutBuilderRoutes.php
@@ -2,17 +2,24 @@
namespace Drupal\layout_builder\Routing;
+use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
-use Symfony\Component\Routing\Route;
+use Drupal\Core\Routing\RouteBuildEvent;
+use Drupal\Core\Routing\RoutingEvents;
+use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
+use Drupal\layout_builder\Field\LayoutSectionItemList;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Provides routes for the Layout Builder UI.
*
* @internal
*/
-class LayoutBuilderRoutes {
+class LayoutBuilderRoutes implements EventSubscriberInterface {
+
+ use LayoutBuilderRoutesTrait;
/**
* The entity type manager.
@@ -64,68 +71,55 @@ class LayoutBuilderRoutes {
$options['parameters'][$entity_type_id]['type'] = 'entity:' . $entity_type_id;
$template = $entity_type->getLinkTemplate('layout-builder');
- $routes += $this->buildRoute('overrides', 'entity.' . $entity_type_id, $template, $defaults, $requirements, $options);
+ $routes += $this->buildRoute(LayoutSectionItemList::class, 'entity.' . $entity_type_id, $template, $defaults, $requirements, $options);
}
return $routes;
}
/**
- * Builds the layout routes for the given values.
- *
- * @param string $type
- * The section storage type.
- * @param string $route_name_prefix
- * The prefix to use for the route name.
- * @param string $path
- * The path patten for the routes.
- * @param array $defaults
- * An array of default parameter values.
- * @param array $requirements
- * An array of requirements for parameters.
- * @param array $options
- * An array of options.
+ * Alters existing routes for a specific collection.
*
- * @return \Symfony\Component\Routing\Route[]
- * An array of route objects.
+ * @param \Drupal\Core\Routing\RouteBuildEvent $event
+ * The route build event.
*/
- protected function buildRoute($type, $route_name_prefix, $path, array $defaults, array $requirements, array $options) {
- $routes = [];
-
- $defaults['section_storage_type'] = $type;
- // Provide an empty value to allow the section storage to be upcast.
- $defaults['section_storage'] = '';
- // Trigger the layout builder access check.
- $requirements['_has_layout_section'] = 'true';
- // Trigger the layout builder RouteEnhancer.
- $options['_layout_builder'] = TRUE;
-
- $main_defaults = $defaults;
- $main_defaults['is_rebuilding'] = FALSE;
- $main_defaults['_controller'] = '\Drupal\layout_builder\Controller\LayoutBuilderController::layout';
- $main_defaults['_title_callback'] = '\Drupal\layout_builder\Controller\LayoutBuilderController::title';
- $route = (new Route($path))
- ->setDefaults($main_defaults)
- ->setRequirements($requirements)
- ->setOptions($options);
- $routes["{$route_name_prefix}.layout_builder"] = $route;
-
- $save_defaults = $defaults;
- $save_defaults['_controller'] = '\Drupal\layout_builder\Controller\LayoutBuilderController::saveLayout';
- $route = (new Route("$path/save"))
- ->setDefaults($save_defaults)
- ->setRequirements($requirements)
- ->setOptions($options);
- $routes["{$route_name_prefix}.layout_builder_save"] = $route;
-
- $cancel_defaults = $defaults;
- $cancel_defaults['_controller'] = '\Drupal\layout_builder\Controller\LayoutBuilderController::cancelLayout';
- $route = (new Route("$path/cancel"))
- ->setDefaults($cancel_defaults)
- ->setRequirements($requirements)
- ->setOptions($options);
- $routes["{$route_name_prefix}.layout_builder_cancel"] = $route;
-
- return $routes;
+ public function onAlterRoutes(RouteBuildEvent $event) {
+ $collection = $event->getRouteCollection();
+ foreach ($this->getEntityTypes() as $entity_type_id => $entity_type) {
+ if ($route_name = $entity_type->get('field_ui_base_route')) {
+ // Try to get the route from the current collection.
+ if (!$entity_route = $collection->get($route_name)) {
+ continue;
+ }
+ $path = $entity_route->getPath() . '/display-layout/{view_mode_name}';
+
+ $defaults = [];
+ $defaults['entity_type_id'] = $entity_type_id;
+ // If the entity type has no bundles and it doesn't use {bundle} in its
+ // admin path, use the entity type.
+ if (strpos($path, '{bundle}') === FALSE) {
+ if (!$entity_type->hasKey('bundle')) {
+ $defaults['bundle'] = $entity_type_id;
+ }
+ else {
+ $defaults['bundle_key'] = $entity_type->getBundleEntityType();
+ }
+ }
+
+ $requirements = [];
+ $requirements['_field_ui_view_mode_access'] = 'administer ' . $entity_type_id . ' display';
+
+ $options['parameters']['section_storage']['layout_builder_tempstore'] = TRUE;
+ // Merge the entity route options in after Layout Builder's.
+ $options = NestedArray::mergeDeep($options, $entity_route->getOptions());
+ // Disable the admin route flag after merging in entity route options.
+ $options['_admin_route'] = FALSE;
+
+ $routes = $this->buildRoute(LayoutBuilderEntityViewDisplay::class, 'entity.entity_view_display.' . $entity_type_id, $path, $defaults, $requirements, $options);
+ foreach ($routes as $name => $route) {
+ $collection->add($name, $route);
+ }
+ }
+ }
}
/**
@@ -154,4 +148,13 @@ class LayoutBuilderRoutes {
});
}
+ /**
+ * {@inheritdoc}
+ */
+ public static function getSubscribedEvents() {
+ // Run after \Drupal\field_ui\Routing\RouteSubscriber.
+ $events[RoutingEvents::ALTER] = ['onAlterRoutes', -110];
+ return $events;
+ }
+
}
diff --git a/core/modules/layout_builder/src/Routing/LayoutBuilderRoutesTrait.php b/core/modules/layout_builder/src/Routing/LayoutBuilderRoutesTrait.php
new file mode 100644
index 0000000..da9e1c1
--- /dev/null
+++ b/core/modules/layout_builder/src/Routing/LayoutBuilderRoutesTrait.php
@@ -0,0 +1,92 @@
+<?php
+
+namespace Drupal\layout_builder\Routing;
+
+use Drupal\layout_builder\OverridesSectionStorageInterface;
+use Drupal\layout_builder\SectionStorageInterface;
+use Symfony\Component\Routing\Route;
+
+/**
+ * Provides a trait for building routes for a Layout Builder UI.
+ *
+ * @internal
+ * Layout Builder is currently experimental and should only be leveraged by
+ * experimental modules and development releases of contributed modules.
+ * See https://www.drupal.org/core/experimental for more information.
+ */
+trait LayoutBuilderRoutesTrait {
+
+ /**
+ * Builds the layout routes for the given values.
+ *
+ * @param string $class
+ * The class defining the section storage.
+ * @param string $route_name_prefix
+ * The prefix to use for the route name.
+ * @param string $path
+ * The path patten for the routes.
+ * @param array $defaults
+ * An array of default parameter values.
+ * @param array $requirements
+ * An array of requirements for parameters.
+ * @param array $options
+ * An array of options.
+ *
+ * @return \Symfony\Component\Routing\Route[]
+ * An array of route objects.
+ */
+ protected function buildRoute($class, $route_name_prefix, $path, array $defaults, array $requirements, array $options) {
+ $routes = [];
+
+ if (!is_subclass_of($class, SectionStorageInterface::class)) {
+ return $routes;
+ }
+
+ $defaults['section_storage_type'] = $class::getStorageType();
+ // Provide an empty value to allow the section storage to be upcast.
+ $defaults['section_storage'] = '';
+ // Trigger the layout builder access check.
+ $requirements['_has_layout_section'] = 'true';
+ // Trigger the layout builder RouteEnhancer.
+ $options['_layout_builder'] = TRUE;
+
+ $main_defaults = $defaults;
+ $main_defaults['is_rebuilding'] = FALSE;
+ $main_defaults['_controller'] = '\Drupal\layout_builder\Controller\LayoutBuilderController::layout';
+ $main_defaults['_title_callback'] = '\Drupal\layout_builder\Controller\LayoutBuilderController::title';
+ $route = (new Route($path))
+ ->setDefaults($main_defaults)
+ ->setRequirements($requirements)
+ ->setOptions($options);
+ $routes["{$route_name_prefix}.layout_builder"] = $route;
+
+ $save_defaults = $defaults;
+ $save_defaults['_controller'] = '\Drupal\layout_builder\Controller\LayoutBuilderController::saveLayout';
+ $route = (new Route("$path/save"))
+ ->setDefaults($save_defaults)
+ ->setRequirements($requirements)
+ ->setOptions($options);
+ $routes["{$route_name_prefix}.layout_builder_save"] = $route;
+
+ $cancel_defaults = $defaults;
+ $cancel_defaults['_controller'] = '\Drupal\layout_builder\Controller\LayoutBuilderController::cancelLayout';
+ $route = (new Route("$path/cancel"))
+ ->setDefaults($cancel_defaults)
+ ->setRequirements($requirements)
+ ->setOptions($options);
+ $routes["{$route_name_prefix}.layout_builder_cancel"] = $route;
+
+ if (is_subclass_of($class, OverridesSectionStorageInterface::class)) {
+ $revert_defaults = $defaults;
+ $revert_defaults['_form'] = '\Drupal\layout_builder\Form\RevertOverridesForm';
+ $route = (new Route("$path/revert"))
+ ->setDefaults($revert_defaults)
+ ->setRequirements($requirements)
+ ->setOptions($options);
+ $routes["{$route_name_prefix}.layout_builder_revert"] = $route;
+ }
+
+ return $routes;
+ }
+
+}
diff --git a/core/modules/layout_builder/src/Routing/SectionStorageDefaultsParamConverter.php b/core/modules/layout_builder/src/Routing/SectionStorageDefaultsParamConverter.php
new file mode 100644
index 0000000..2f88610
--- /dev/null
+++ b/core/modules/layout_builder/src/Routing/SectionStorageDefaultsParamConverter.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace Drupal\layout_builder\Routing;
+
+use Drupal\Core\ParamConverter\EntityConverter;
+
+/**
+ * Provides a param converter for defaults-based section storage.
+ *
+ * @internal
+ * Layout Builder is currently experimental and should only be leveraged by
+ * experimental modules and development releases of contributed modules.
+ * See https://www.drupal.org/core/experimental for more information.
+ */
+class SectionStorageDefaultsParamConverter extends EntityConverter implements SectionStorageParamConverterInterface {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function convert($value, $definition, $name, array $defaults) {
+ if (!$value) {
+ // If a bundle is not provided but a value corresponding to the bundle key
+ // is, use that for the bundle value.
+ if (empty($defaults['bundle']) && isset($defaults['bundle_key']) && !empty($defaults[$defaults['bundle_key']])) {
+ $defaults['bundle'] = $defaults[$defaults['bundle_key']];
+ }
+
+ if (empty($defaults['entity_type_id']) && empty($defaults['bundle']) && empty($defaults['view_mode_name'])) {
+ return NULL;
+ }
+
+ $value = $defaults['entity_type_id'] . '.' . $defaults['bundle'] . '.' . $defaults['view_mode_name'];
+ }
+ if (!$display = parent::convert($value, $definition, $name, $defaults)) {
+ list($entity_type_id, $bundle, $view_mode) = explode('.', $value);
+ $display = $this->entityManager->getStorage('entity_view_display')->create([
+ 'targetEntityType' => $entity_type_id,
+ 'bundle' => $bundle,
+ 'mode' => $view_mode,
+ 'status' => TRUE,
+ ]);
+ }
+ return $display;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getEntityTypeFromDefaults($definition, $name, array $defaults) {
+ return 'entity_view_display';
+ }
+
+}
diff --git a/core/modules/layout_builder/src/Section.php b/core/modules/layout_builder/src/Section.php
index 02f274e..04b1cbb 100644
--- a/core/modules/layout_builder/src/Section.php
+++ b/core/modules/layout_builder/src/Section.php
@@ -68,14 +68,16 @@ class Section {
*
* @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts
* An array of available contexts.
+ * @param bool $in_preview
+ * TRUE if the section is being previewed, FALSE otherwise.
*
* @return array
* A renderable array representing the content of the section.
*/
- public function toRenderArray(array $contexts = []) {
+ public function toRenderArray(array $contexts = [], $in_preview = FALSE) {
$regions = [];
foreach ($this->getComponents() as $component) {
- if ($output = $component->toRenderArray($contexts)) {
+ if ($output = $component->toRenderArray($contexts, $in_preview)) {
$regions[$component->getRegion()][$component->getUuid()] = $output;
}
}
@@ -133,6 +135,16 @@ class Section {
}
/**
+ * Gets the default region.
+ *
+ * @return string
+ * The machine-readable name of the default region.
+ */
+ public function getDefaultRegion() {
+ return $this->layoutPluginManager()->getDefinition($this->getLayoutId())->getDefaultRegion();
+ }
+
+ /**
* Returns the components of the section.
*
* @return \Drupal\layout_builder\SectionComponent[]
@@ -307,4 +319,23 @@ class Section {
return \Drupal::service('plugin.manager.core.layout');
}
+ /**
+ * Returns an array representation of the section.
+ *
+ * @internal
+ * This is intended for use by a storage mechanism for sections.
+ *
+ * @return array
+ * An array representation of the section component.
+ */
+ public function toArray() {
+ return [
+ 'layout_id' => $this->getLayoutId(),
+ 'layout_settings' => $this->getLayoutSettings(),
+ 'components' => array_map(function (SectionComponent $component) {
+ return $component->toArray();
+ }, $this->getComponents()),
+ ];
+ }
+
}
diff --git a/core/modules/layout_builder/src/SectionComponent.php b/core/modules/layout_builder/src/SectionComponent.php
index 6a0c238..7af03af 100644
--- a/core/modules/layout_builder/src/SectionComponent.php
+++ b/core/modules/layout_builder/src/SectionComponent.php
@@ -90,11 +90,13 @@ class SectionComponent {
*
* @param \Drupal\Core\Plugin\Context\ContextInterface[] $contexts
* An array of available contexts.
+ * @param bool $in_preview
+ * TRUE if the component is being previewed, FALSE otherwise.
*
* @return array
* A renderable array representing the content of the component.
*/
- public function toRenderArray(array $contexts = []) {
+ public function toRenderArray(array $contexts = [], $in_preview = FALSE) {
$output = [];
$plugin = $this->getPlugin($contexts);
@@ -104,7 +106,7 @@ class SectionComponent {
$access = $plugin->access($this->currentUser(), TRUE);
$cacheability = CacheableMetadata::createFromObject($access);
- if ($access->isAllowed()) {
+ if ($in_preview || $access->isAllowed()) {
$cacheability->addCacheableDependency($plugin);
// @todo Move this to BlockBase in https://www.drupal.org/node/2931040.
$output = [
@@ -242,7 +244,7 @@ class SectionComponent {
* @throws \Drupal\Component\Plugin\Exception\PluginException
* Thrown if the plugin ID cannot be found.
*/
- protected function getPluginId() {
+ public function getPluginId() {
if (empty($this->configuration['id'])) {
throw new PluginException(sprintf('No plugin ID specified for component with "%s" UUID', $this->uuid));
}
@@ -306,4 +308,23 @@ class SectionComponent {
return \Drupal::currentUser();
}
+ /**
+ * Returns an array representation of the section component.
+ *
+ * @internal
+ * This is intended for use by a storage mechanism for section components.
+ *
+ * @return array
+ * An array representation of the section component.
+ */
+ public function toArray() {
+ return [
+ 'uuid' => $this->getUuid(),
+ 'region' => $this->getRegion(),
+ 'configuration' => $this->getConfiguration(),
+ 'additional' => $this->additional,
+ 'weight' => $this->getWeight(),
+ ];
+ }
+
}
diff --git a/core/modules/layout_builder/src/SectionStorageInterface.php b/core/modules/layout_builder/src/SectionStorageInterface.php
index 13217d8..cc1efab 100644
--- a/core/modules/layout_builder/src/SectionStorageInterface.php
+++ b/core/modules/layout_builder/src/SectionStorageInterface.php
@@ -18,7 +18,7 @@ interface SectionStorageInterface extends \Countable {
* Gets the layout sections.
*
* @return \Drupal\layout_builder\Section[]
- * An array of sections.
+ * A sequentially and numerically keyed array of section objects.
*/
public function getSections();
@@ -61,6 +61,9 @@ interface SectionStorageInterface extends \Countable {
/**
* Removes the section at the given delta.
*
+ * As sections are stored sequentially and numerically this will re-key every
+ * subsequent section, shifting them forward.
+ *
* @param int $delta
* The delta of the section.
*
@@ -92,7 +95,7 @@ interface SectionStorageInterface extends \Countable {
* @return string
* The type of storage.
*/
- public function getStorageType();
+ public static function getStorageType();
/**
* Gets the label for the object using the sections.
diff --git a/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php
new file mode 100644
index 0000000..63c084c
--- /dev/null
+++ b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php
@@ -0,0 +1,232 @@
+<?php
+
+namespace Drupal\Tests\layout_builder\Functional;
+
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * Tests the Layout Builder UI.
+ *
+ * @group layout_builder
+ */
+class LayoutBuilderTest extends BrowserTestBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ public static $modules = [
+ 'layout_builder',
+ 'block',
+ 'node',
+ ];
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp() {
+ parent::setUp();
+
+ // @todo The Layout Builder UI relies on local tasks; fix in
+ // https://www.drupal.org/project/drupal/issues/2917777.
+ $this->drupalPlaceBlock('local_tasks_block');
+
+ // Create two nodes.
+ $this->createContentType(['type' => 'bundle_with_section_field']);
+ $this->createNode([
+ 'type' => 'bundle_with_section_field',
+ 'title' => 'The first node title',
+ 'body' => [
+ [
+ 'value' => 'The first node body',
+ ],
+ ],
+ ]);
+ $this->createNode([
+ 'type' => 'bundle_with_section_field',
+ 'title' => 'The second node title',
+ 'body' => [
+ [
+ 'value' => 'The second node body',
+ ],
+ ],
+ ]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function testLayoutBuilderUi() {
+ $assert_session = $this->assertSession();
+ $page = $this->getSession()->getPage();
+
+ $this->drupalLogin($this->drupalCreateUser([
+ 'configure any layout',
+ 'administer node display',
+ 'administer node fields',
+ ]));
+
+ $this->drupalGet('node/1');
+ $assert_session->pageTextContains('The first node body');
+ $assert_session->pageTextNotContains('Powered by Drupal');
+ $assert_session->linkNotExists('Layout');
+
+ $field_ui_prefix = 'admin/structure/types/manage/bundle_with_section_field';
+
+ // From the manage display page, go to manage the layout.
+ $this->drupalGet("$field_ui_prefix/display/default");
+ $assert_session->linkExists('Manage layout');
+ $this->clickLink('Manage layout');
+ $assert_session->addressEquals("$field_ui_prefix/display-layout/default");
+ // The body field is present.
+ $assert_session->elementExists('css', '.field--name-body');
+
+ // Add a new block.
+ $assert_session->linkExists('Add Block');
+ $this->clickLink('Add Block');
+ $assert_session->linkExists('Powered by Drupal');
+ $this->clickLink('Powered by Drupal');
+ $page->fillField('settings[label]', 'This is the label');
+ $page->checkField('settings[label_display]');
+ $page->pressButton('Add Block');
+ $assert_session->pageTextContains('Powered by Drupal');
+ $assert_session->pageTextContains('This is the label');
+ $assert_session->addressEquals("$field_ui_prefix/display-layout/default");
+
+ // Save the defaults.
+ $assert_session->linkExists('Save Layout');
+ $this->clickLink('Save Layout');
+ $assert_session->addressEquals("$field_ui_prefix/display/default");
+
+ // The node uses the defaults, no overrides available.
+ $this->drupalGet('node/1');
+ $assert_session->pageTextContains('The first node body');
+ $assert_session->pageTextContains('Powered by Drupal');
+ $assert_session->linkNotExists('Layout');
+
+ // Enable overrides.
+ $this->drupalPostForm("$field_ui_prefix/display/default", ['layout[allow_custom]' => TRUE], 'Save');
+ $this->drupalGet('node/1');
+
+ // Remove the section from the defaults.
+ $assert_session->linkExists('Layout');
+ $this->clickLink('Layout');
+ $assert_session->linkExists('Remove section');
+ $this->clickLink('Remove section');
+ $page->pressButton('Remove');
+
+ // Add a new section.
+ $this->clickLink('Add Section');
+ $assert_session->linkExists('Two column');
+ $this->clickLink('Two column');
+ $assert_session->linkExists('Save Layout');
+ $this->clickLink('Save Layout');
+ $assert_session->pageTextNotContains('The first node body');
+ $assert_session->pageTextNotContains('Powered by Drupal');
+
+ // Alter the defaults.
+ $this->drupalGet("$field_ui_prefix/display-layout/default");
+ $assert_session->linkExists('Add Block');
+ $this->clickLink('Add Block');
+ $assert_session->linkExists('Title');
+ $this->clickLink('Title');
+ $page->pressButton('Add Block');
+ // The title field is present.
+ $assert_session->elementExists('css', '.field--name-title');
+ $this->clickLink('Save Layout');
+
+ // View the other node, which is still using the defaults.
+ $this->drupalGet('node/2');
+ $assert_session->pageTextContains('The second node title');
+ $assert_session->pageTextContains('The second node body');
+ $assert_session->pageTextContains('Powered by Drupal');
+
+ // The overridden node does not pick up the changes to defaults.
+ $this->drupalGet('node/1');
+ $assert_session->elementNotExists('css', '.field--name-title');
+ $assert_session->pageTextNotContains('The first node body');
+ $assert_session->pageTextNotContains('Powered by Drupal');
+ $assert_session->linkExists('Layout');
+
+ // Reverting the override returns it to the defaults.
+ $this->clickLink('Layout');
+ $assert_session->linkExists('Revert to defaults');
+ $this->clickLink('Revert to defaults');
+ $page->pressButton('Revert');
+ $assert_session->pageTextContains('The layout has been reverted back to defaults.');
+ $assert_session->elementExists('css', '.field--name-title');
+ $assert_session->pageTextContains('The first node body');
+ $assert_session->pageTextContains('Powered by Drupal');
+
+ // Add a new field.
+ $edit = [
+ 'new_storage_type' => 'string',
+ 'label' => 'My text field',
+ 'field_name' => 'my_text',
+ ];
+ $this->drupalPostForm("$field_ui_prefix/fields/add-field", $edit, 'Save and continue');
+ $page->pressButton('Save field settings');
+ $page->pressButton('Save settings');
+ $this->drupalGet("$field_ui_prefix/display-layout/default");
+ $assert_session->pageTextContains('My text field');
+ $assert_session->elementExists('css', '.field--name-field-my-text');
+
+ // Delete the field.
+ $this->drupalPostForm("$field_ui_prefix/fields/node.bundle_with_section_field.field_my_text/delete", [], 'Delete');
+ $this->drupalGet("$field_ui_prefix/display-layout/default");
+ $assert_session->pageTextNotContains('My text field');
+ $assert_session->elementNotExists('css', '.field--name-field-my-text');
+ }
+
+ /**
+ * Tests that component's dependencies are respected during removal.
+ */
+ public function testPluginDependencies() {
+ $assert_session = $this->assertSession();
+ $page = $this->getSession()->getPage();
+
+ $this->container->get('module_installer')->install(['menu_ui']);
+ $this->drupalLogin($this->drupalCreateUser([
+ 'configure any layout',
+ 'administer node display',
+ 'administer menu',
+ ]));
+
+ // Create a new menu.
+ $this->drupalGet('admin/structure/menu/add');
+ $page->fillField('label', 'My Menu');
+ $page->fillField('id', 'mymenu');
+ $page->pressButton('Save');
+
+ // Add a menu block.
+ $this->drupalGet('admin/structure/types/manage/bundle_with_section_field/display-layout/default');
+ $assert_session->linkExists('Add Block');
+ $this->clickLink('Add Block');
+ $assert_session->linkExists('My Menu');
+ $this->clickLink('My Menu');
+ $page->pressButton('Add Block');
+
+ // Add another block alongside the menu.
+ $assert_session->linkExists('Add Block');
+ $this->clickLink('Add Block');
+ $assert_session->linkExists('Powered by Drupal');
+ $this->clickLink('Powered by Drupal');
+ $page->pressButton('Add Block');
+
+ // Assert that the blocks are visible, and save the layout.
+ $assert_session->pageTextContains('Powered by Drupal');
+ $assert_session->pageTextContains('My Menu');
+ $assert_session->elementExists('css', '.block.menu--mymenu');
+ $assert_session->linkExists('Save Layout');
+ $this->clickLink('Save Layout');
+
+ // Delete the menu.
+ $this->drupalPostForm('admin/structure/menu/manage/mymenu/delete', [], 'Delete');
+
+ // Ensure that the menu block is gone, but that the other block remains.
+ $this->drupalGet('admin/structure/types/manage/bundle_with_section_field/display-layout/default');
+ $assert_session->pageTextContains('Powered by Drupal');
+ $assert_session->pageTextNotContains('My Menu');
+ $assert_session->elementNotExists('css', '.block.menu--mymenu');
+ }
+
+}
diff --git a/core/modules/layout_builder/tests/src/Functional/LayoutSectionTest.php b/core/modules/layout_builder/tests/src/Functional/LayoutSectionTest.php
index b2b5f9c..e0dfdc7 100644
--- a/core/modules/layout_builder/tests/src/Functional/LayoutSectionTest.php
+++ b/core/modules/layout_builder/tests/src/Functional/LayoutSectionTest.php
@@ -2,8 +2,8 @@
namespace Drupal\Tests\layout_builder\Functional;
-use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\language\Entity\ConfigurableLanguage;
+use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
use Drupal\layout_builder\Section;
use Drupal\layout_builder\SectionComponent;
use Drupal\Tests\BrowserTestBase;
@@ -18,7 +18,7 @@ class LayoutSectionTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
- public static $modules = ['layout_builder', 'node', 'block_test'];
+ public static $modules = ['field_ui', 'layout_builder', 'node', 'block_test'];
/**
* The name of the layout section field.
@@ -34,19 +34,21 @@ class LayoutSectionTest extends BrowserTestBase {
parent::setUp();
$this->createContentType([
- 'type' => 'bundle_with_section_field',
+ 'type' => 'bundle_without_section_field',
]);
$this->createContentType([
- 'type' => 'bundle_without_section_field',
+ 'type' => 'bundle_with_section_field',
]);
- layout_builder_add_layout_section_field('node', 'bundle_with_section_field');
- $display = EntityViewDisplay::load('node.bundle_with_section_field.default');
- $display->setThirdPartySetting('layout_builder', 'allow_custom', TRUE);
- $display->save();
+ LayoutBuilderEntityViewDisplay::load('node.bundle_with_section_field.default')
+ ->setOverridable()
+ ->save();
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
+ 'administer node display',
+ 'administer node fields',
+ 'administer content types',
], 'foobar'));
}
@@ -85,7 +87,7 @@ class LayoutSectionTest extends BrowserTestBase {
[
'section' => new Section('layout_onecol', [], [
'baz' => new SectionComponent('baz', 'content', [
- 'id' => 'field_block:node:body',
+ 'id' => 'field_block:node:bundle_with_section_field:body',
'context_mapping' => [
'entity' => 'layout_builder.entity',
],
@@ -277,6 +279,46 @@ class LayoutSectionTest extends BrowserTestBase {
}
/**
+ * Tests that deleting a field removes it from the layout.
+ */
+ public function testLayoutDeletingField() {
+ $assert_session = $this->assertSession();
+
+ $this->drupalGet('/admin/structure/types/manage/bundle_with_section_field/display-layout/default');
+ $assert_session->statusCodeEquals(200);
+ $assert_session->elementExists('css', '.field--name-body');
+
+ // Delete the field from both bundles.
+ $this->drupalGet('/admin/structure/types/manage/bundle_without_section_field/fields/node.bundle_without_section_field.body/delete');
+ $this->submitForm([], 'Delete');
+ $this->drupalGet('/admin/structure/types/manage/bundle_with_section_field/display-layout/default');
+ $assert_session->statusCodeEquals(200);
+ $assert_session->elementExists('css', '.field--name-body');
+
+ $this->drupalGet('/admin/structure/types/manage/bundle_with_section_field/fields/node.bundle_with_section_field.body/delete');
+ $this->submitForm([], 'Delete');
+ $this->drupalGet('/admin/structure/types/manage/bundle_with_section_field/display-layout/default');
+ $assert_session->statusCodeEquals(200);
+ $assert_session->elementNotExists('css', '.field--name-body');
+ }
+
+ /**
+ * Tests that deleting a bundle removes the layout.
+ */
+ public function testLayoutDeletingBundle() {
+ $assert_session = $this->assertSession();
+
+ $display = LayoutBuilderEntityViewDisplay::load('node.bundle_with_section_field.default');
+ $this->assertInstanceOf(LayoutBuilderEntityViewDisplay::class, $display);
+
+ $this->drupalPostForm('/admin/structure/types/manage/bundle_with_section_field/delete', [], 'Delete');
+ $assert_session->statusCodeEquals(200);
+
+ $display = LayoutBuilderEntityViewDisplay::load('node.bundle_with_section_field.default');
+ $this->assertNull($display);
+ }
+
+ /**
* Asserts the output of a layout section.
*
* @param string|array $expected_selector
diff --git a/core/modules/layout_builder/tests/src/FunctionalJavascript/FieldBlockTest.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/FieldBlockTest.php
index e7ae850..4f086d6 100644
--- a/core/modules/layout_builder/tests/src/FunctionalJavascript/FieldBlockTest.php
+++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/FieldBlockTest.php
@@ -67,7 +67,7 @@ class FieldBlockTest extends JavascriptTestBase {
$assert_session->pageTextNotContains('Initial email');
$assert_session->pageTextContains('Date field');
- $block_url = 'admin/structure/block/add/field_block%3Auser%3Afield_date/classy';
+ $block_url = 'admin/structure/block/add/field_block%3Auser%3Auser%3Afield_date/classy';
$assert_session->linkByHrefExists($block_url);
$this->drupalGet($block_url);
@@ -112,6 +112,7 @@ class FieldBlockTest extends JavascriptTestBase {
];
$config = $this->container->get('config.factory')->get('block.block.datefield');
$this->assertEquals($expected, $config->get('settings.formatter'));
+ $this->assertEquals(['field.field.user.user.field_date'], $config->get('dependencies.config'));
// Assert that the block is displaying the user field.
$this->drupalGet('admin');
diff --git a/core/modules/layout_builder/tests/src/Kernel/FieldBlockTest.php b/core/modules/layout_builder/tests/src/Kernel/FieldBlockTest.php
index a118cf5..8c529cc 100644
--- a/core/modules/layout_builder/tests/src/Kernel/FieldBlockTest.php
+++ b/core/modules/layout_builder/tests/src/Kernel/FieldBlockTest.php
@@ -186,7 +186,7 @@ class FieldBlockTest extends EntityKernelTestBase {
$block = new FieldBlock(
$configuration,
- 'field_block:entity_test:the_field_name',
+ 'field_block:entity_test:entity_test:the_field_name',
$plugin_definition,
$entity_field_manager->reveal(),
$formatter_manager->reveal(),
diff --git a/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderCompatibilityTestBase.php b/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderCompatibilityTestBase.php
index 6c376fa..d5a4cd0 100644
--- a/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderCompatibilityTestBase.php
+++ b/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderCompatibilityTestBase.php
@@ -42,6 +42,7 @@ abstract class LayoutBuilderCompatibilityTestBase extends EntityKernelTestBase {
$this->installEntitySchema('entity_test_base_field_display');
$this->installConfig(['filter']);
+ $this->installSchema('system', ['key_value_expire']);
// Set up a non-admin user that is allowed to view test entities.
\Drupal::currentUser()->setAccount($this->createUser(['uid' => 2], ['view test entity']));
@@ -68,7 +69,7 @@ abstract class LayoutBuilderCompatibilityTestBase extends EntityKernelTestBase {
'status' => TRUE,
]);
$this->display
- ->setComponent('test_field_display_configurable', ['region' => 'content', 'weight' => 5])
+ ->setComponent('test_field_display_configurable', ['weight' => 5])
->save();
// Create an entity with fields that are configurable and non-configurable.
@@ -92,7 +93,7 @@ abstract class LayoutBuilderCompatibilityTestBase extends EntityKernelTestBase {
$this->refreshServices();
$this->display = $this->reloadEntity($this->display);
- $this->display->setThirdPartySetting('layout_builder', 'allow_custom', TRUE)->save();
+ $this->display->setOverridable()->save();
$this->entity = $this->reloadEntity($this->entity);
}
diff --git a/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderEntityViewDisplayTest.php b/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderEntityViewDisplayTest.php
new file mode 100644
index 0000000..034a766
--- /dev/null
+++ b/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderEntityViewDisplayTest.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace Drupal\Tests\layout_builder\Kernel;
+
+use Drupal\Core\Config\Schema\SchemaIncompleteException;
+use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
+
+/**
+ * @coversDefaultClass \Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay
+ *
+ * @group layout_builder
+ */
+class LayoutBuilderEntityViewDisplayTest extends SectionStorageTestBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getSectionStorage(array $section_data) {
+ $display = LayoutBuilderEntityViewDisplay::create([
+ 'targetEntityType' => 'entity_test',
+ 'bundle' => 'entity_test',
+ 'mode' => 'default',
+ 'status' => TRUE,
+ 'third_party_settings' => [
+ 'layout_builder' => [
+ 'sections' => $section_data,
+ ],
+ ],
+ ]);
+ $display->save();
+ return $display;
+ }
+
+ /**
+ * @covers ::getSection
+ */
+ public function testGetSectionInvalidDelta() {
+ $this->setExpectedException(\OutOfBoundsException::class, 'Invalid delta "2" for the "entity_test.entity_test.default"');
+ $this->sectionStorage->getSection(2);
+ }
+
+ /**
+ * Tests that configuration schema enforces valid values.
+ */
+ public function testInvalidConfiguration() {
+ $this->setExpectedException(SchemaIncompleteException::class);
+ $this->sectionStorage->getSection(0)->getComponent('first-uuid')->setConfiguration(['id' => 'foo', 'bar' => 'baz']);
+ $this->sectionStorage->save();
+ }
+
+}
diff --git a/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderInstallTest.php b/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderInstallTest.php
index 3873af8..fa646df 100644
--- a/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderInstallTest.php
+++ b/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderInstallTest.php
@@ -2,6 +2,8 @@
namespace Drupal\Tests\layout_builder\Kernel;
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
use Drupal\layout_builder\Section;
/**
@@ -49,6 +51,40 @@ class LayoutBuilderInstallTest extends LayoutBuilderCompatibilityTestBase {
$this->entity->get('layout_builder__layout')->removeSection(0);
$this->entity->save();
$this->assertFieldAttributes($this->entity, $expected_fields);
+
+ // Test that adding a new field after Layout Builder has been installed will
+ // add the new field to the default region of the first section.
+ $field_storage = FieldStorageConfig::create([
+ 'entity_type' => 'entity_test_base_field_display',
+ 'field_name' => 'test_field_display_post_install',
+ 'type' => 'text',
+ ]);
+ $field_storage->save();
+ FieldConfig::create([
+ 'field_storage' => $field_storage,
+ 'bundle' => 'entity_test_base_field_display',
+ 'label' => 'FieldConfig with configurable display',
+ ])->save();
+
+ $this->entity = $this->reloadEntity($this->entity);
+ $this->entity->test_field_display_post_install = 'Test string';
+ $this->entity->save();
+
+ $this->display = $this->reloadEntity($this->display);
+ $this->display
+ ->setComponent('test_field_display_post_install', ['weight' => 50])
+ ->save();
+ $new_expected_fields = [
+ 'field field--name-name field--type-string field--label-hidden field__item',
+ 'field field--name-test-field-display-configurable field--type-boolean field--label-above',
+ 'clearfix text-formatted field field--name-test-display-configurable field--type-text field--label-above',
+ 'clearfix text-formatted field field--name-test-field-display-post-install field--type-text field--label-above',
+ 'clearfix text-formatted field field--name-test-display-non-configurable field--type-text field--label-above',
+ 'clearfix text-formatted field field--name-test-display-multiple field--type-text field--label-above',
+ ];
+ $this->assertFieldAttributes($this->entity, $new_expected_fields);
+ $this->assertNotEmpty($this->cssSelect('.layout--onecol'));
+ $this->assertText('Test string');
}
}
diff --git a/core/modules/layout_builder/tests/src/Kernel/LayoutSectionItemListTest.php b/core/modules/layout_builder/tests/src/Kernel/LayoutSectionItemListTest.php
index f3a6e6f..af8395f 100644
--- a/core/modules/layout_builder/tests/src/Kernel/LayoutSectionItemListTest.php
+++ b/core/modules/layout_builder/tests/src/Kernel/LayoutSectionItemListTest.php
@@ -3,6 +3,7 @@
namespace Drupal\Tests\layout_builder\Kernel;
use Drupal\entity_test\Entity\EntityTestBaseFieldDisplay;
+use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
/**
* Tests the field type for Layout Sections.
@@ -26,7 +27,12 @@ class LayoutSectionItemListTest extends SectionStorageTestBase {
*/
protected function getSectionStorage(array $section_data) {
$this->installEntitySchema('entity_test_base_field_display');
- layout_builder_add_layout_section_field('entity_test_base_field_display', 'entity_test_base_field_display');
+ LayoutBuilderEntityViewDisplay::create([
+ 'targetEntityType' => 'entity_test_base_field_display',
+ 'bundle' => 'entity_test_base_field_display',
+ 'mode' => 'default',
+ 'status' => TRUE,
+ ])->setOverridable()->save();
array_map(function ($row) {
return ['section' => $row];
diff --git a/core/modules/layout_builder/tests/src/Kernel/SectionStorageTestBase.php b/core/modules/layout_builder/tests/src/Kernel/SectionStorageTestBase.php
index 97c1fba..151cf73 100644
--- a/core/modules/layout_builder/tests/src/Kernel/SectionStorageTestBase.php
+++ b/core/modules/layout_builder/tests/src/Kernel/SectionStorageTestBase.php
@@ -2,14 +2,14 @@
namespace Drupal\Tests\layout_builder\Kernel;
-use Drupal\KernelTests\KernelTestBase;
+use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
use Drupal\layout_builder\Section;
use Drupal\layout_builder\SectionComponent;
/**
* Provides a base class for testing implementations of section storage.
*/
-abstract class SectionStorageTestBase extends KernelTestBase {
+abstract class SectionStorageTestBase extends EntityKernelTestBase {
/**
* {@inheritdoc}
@@ -18,8 +18,6 @@ abstract class SectionStorageTestBase extends KernelTestBase {
'layout_builder',
'layout_discovery',
'layout_test',
- 'user',
- 'entity_test',
];
/**
@@ -35,12 +33,14 @@ abstract class SectionStorageTestBase extends KernelTestBase {
protected function setUp() {
parent::setUp();
+ $this->installSchema('system', ['key_value_expire']);
+
$section_data = [
new Section('layout_test_plugin', [], [
- 'first-uuid' => new SectionComponent('first-uuid', 'content'),
+ 'first-uuid' => new SectionComponent('first-uuid', 'content', ['id' => 'foo']),
]),
new Section('layout_test_plugin', ['setting_1' => 'bar'], [
- 'second-uuid' => new SectionComponent('second-uuid', 'content'),
+ 'second-uuid' => new SectionComponent('second-uuid', 'content', ['id' => 'foo']),
]),
];
$this->sectionStorage = $this->getSectionStorage($section_data);
@@ -63,10 +63,10 @@ abstract class SectionStorageTestBase extends KernelTestBase {
public function testGetSections() {
$expected = [
new Section('layout_test_plugin', [], [
- 'first-uuid' => new SectionComponent('first-uuid', 'content'),
+ 'first-uuid' => new SectionComponent('first-uuid', 'content', ['id' => 'foo']),
]),
new Section('layout_test_plugin', ['setting_1' => 'bar'], [
- 'second-uuid' => new SectionComponent('second-uuid', 'content'),
+ 'second-uuid' => new SectionComponent('second-uuid', 'content', ['id' => 'foo']),
]),
];
$this->assertSections($expected);
@@ -93,11 +93,11 @@ abstract class SectionStorageTestBase extends KernelTestBase {
public function testInsertSection() {
$expected = [
new Section('layout_test_plugin', [], [
- 'first-uuid' => new SectionComponent('first-uuid', 'content'),
+ 'first-uuid' => new SectionComponent('first-uuid', 'content', ['id' => 'foo']),
]),
new Section('setting_1'),
new Section('layout_test_plugin', ['setting_1' => 'bar'], [
- 'second-uuid' => new SectionComponent('second-uuid', 'content'),
+ 'second-uuid' => new SectionComponent('second-uuid', 'content', ['id' => 'foo']),
]),
];
@@ -111,10 +111,10 @@ abstract class SectionStorageTestBase extends KernelTestBase {
public function testAppendSection() {
$expected = [
new Section('layout_test_plugin', [], [
- 'first-uuid' => new SectionComponent('first-uuid', 'content'),
+ 'first-uuid' => new SectionComponent('first-uuid', 'content', ['id' => 'foo']),
]),
new Section('layout_test_plugin', ['setting_1' => 'bar'], [
- 'second-uuid' => new SectionComponent('second-uuid', 'content'),
+ 'second-uuid' => new SectionComponent('second-uuid', 'content', ['id' => 'foo']),
]),
new Section('foo'),
];
@@ -129,7 +129,7 @@ abstract class SectionStorageTestBase extends KernelTestBase {
public function testRemoveSection() {
$expected = [
new Section('layout_test_plugin', ['setting_1' => 'bar'], [
- 'second-uuid' => new SectionComponent('second-uuid', 'content'),
+ 'second-uuid' => new SectionComponent('second-uuid', 'content', ['id' => 'foo']),
]),
];
diff --git a/core/modules/layout_builder/tests/src/Unit/LayoutBuilderRoutesTest.php b/core/modules/layout_builder/tests/src/Unit/LayoutBuilderRoutesTest.php
index e209f6a..961b870 100644
--- a/core/modules/layout_builder/tests/src/Unit/LayoutBuilderRoutesTest.php
+++ b/core/modules/layout_builder/tests/src/Unit/LayoutBuilderRoutesTest.php
@@ -6,9 +6,11 @@ use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityType;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\Routing\RouteBuildEvent;
use Drupal\layout_builder\Routing\LayoutBuilderRoutes;
use Drupal\Tests\UnitTestCase;
use Symfony\Component\Routing\Route;
+use Symfony\Component\Routing\RouteCollection;
/**
* @coversDefaultClass \Drupal\layout_builder\Routing\LayoutBuilderRoutes
@@ -146,6 +148,25 @@ class LayoutBuilderRoutesTest extends UnitTestCase {
'_layout_builder' => TRUE,
]
),
+ 'entity.with_link_template.layout_builder_revert' => new Route(
+ '/entity/{entity}/layout/revert',
+ [
+ 'entity_type_id' => 'with_link_template',
+ 'section_storage_type' => 'overrides',
+ 'section_storage' => '',
+ '_form' => '\Drupal\layout_builder\Form\RevertOverridesForm',
+ ],
+ [
+ '_has_layout_section' => 'true',
+ ],
+ [
+ 'parameters' => [
+ 'section_storage' => ['layout_builder_tempstore' => TRUE],
+ 'with_link_template' => ['type' => 'entity:with_link_template'],
+ ],
+ '_layout_builder' => TRUE,
+ ]
+ ),
'entity.with_integer_id.layout_builder' => new Route(
'/entity/{entity}/layout',
[
@@ -208,6 +229,26 @@ class LayoutBuilderRoutesTest extends UnitTestCase {
'_layout_builder' => TRUE,
]
),
+ 'entity.with_integer_id.layout_builder_revert' => new Route(
+ '/entity/{entity}/layout/revert',
+ [
+ 'entity_type_id' => 'with_integer_id',
+ 'section_storage_type' => 'overrides',
+ 'section_storage' => '',
+ '_form' => '\Drupal\layout_builder\Form\RevertOverridesForm',
+ ],
+ [
+ '_has_layout_section' => 'true',
+ 'with_integer_id' => '\d+',
+ ],
+ [
+ 'parameters' => [
+ 'section_storage' => ['layout_builder_tempstore' => TRUE],
+ 'with_integer_id' => ['type' => 'entity:with_integer_id'],
+ ],
+ '_layout_builder' => TRUE,
+ ]
+ ),
'entity.with_field_ui_route.layout_builder' => new Route(
'/entity/{entity}/layout',
[
@@ -270,6 +311,26 @@ class LayoutBuilderRoutesTest extends UnitTestCase {
'_layout_builder' => TRUE,
]
),
+ 'entity.with_field_ui_route.layout_builder_revert' => new Route(
+ '/entity/{entity}/layout/revert',
+ [
+ 'entity_type_id' => 'with_field_ui_route',
+ 'section_storage_type' => 'overrides',
+ 'section_storage' => '',
+ '_form' => '\Drupal\layout_builder\Form\RevertOverridesForm',
+ ],
+ [
+ '_has_layout_section' => 'true',
+ 'with_field_ui_route' => '\d+',
+ ],
+ [
+ 'parameters' => [
+ 'section_storage' => ['layout_builder_tempstore' => TRUE],
+ 'with_field_ui_route' => ['type' => 'entity:with_field_ui_route'],
+ ],
+ '_layout_builder' => TRUE,
+ ]
+ ),
'entity.with_bundle_key.layout_builder' => new Route(
'/entity/{entity}/layout',
[
@@ -332,6 +393,26 @@ class LayoutBuilderRoutesTest extends UnitTestCase {
'_layout_builder' => TRUE,
]
),
+ 'entity.with_bundle_key.layout_builder_revert' => new Route(
+ '/entity/{entity}/layout/revert',
+ [
+ 'entity_type_id' => 'with_bundle_key',
+ 'section_storage_type' => 'overrides',
+ 'section_storage' => '',
+ '_form' => '\Drupal\layout_builder\Form\RevertOverridesForm',
+ ],
+ [
+ '_has_layout_section' => 'true',
+ 'with_bundle_key' => '\d+',
+ ],
+ [
+ 'parameters' => [
+ 'section_storage' => ['layout_builder_tempstore' => TRUE],
+ 'with_bundle_key' => ['type' => 'entity:with_bundle_key'],
+ ],
+ '_layout_builder' => TRUE,
+ ]
+ ),
'entity.with_bundle_parameter.layout_builder' => new Route(
'/entity/{entity}/layout',
[
@@ -394,9 +475,242 @@ class LayoutBuilderRoutesTest extends UnitTestCase {
'_layout_builder' => TRUE,
]
),
+ 'entity.with_bundle_parameter.layout_builder_revert' => new Route(
+ '/entity/{entity}/layout/revert',
+ [
+ 'entity_type_id' => 'with_bundle_parameter',
+ 'section_storage_type' => 'overrides',
+ 'section_storage' => '',
+ '_form' => '\Drupal\layout_builder\Form\RevertOverridesForm',
+ ],
+ [
+ '_has_layout_section' => 'true',
+ 'with_bundle_parameter' => '\d+',
+ ],
+ [
+ 'parameters' => [
+ 'section_storage' => ['layout_builder_tempstore' => TRUE],
+ 'with_bundle_parameter' => ['type' => 'entity:with_bundle_parameter'],
+ ],
+ '_layout_builder' => TRUE,
+ ]
+ ),
];
$this->assertEquals($expected, $this->routeBuilder->getRoutes());
}
+ /**
+ * @covers ::onAlterRoutes
+ * @covers ::buildRoute
+ * @covers ::hasIntegerId
+ * @covers ::getEntityTypes
+ */
+ public function testOnAlterRoutes() {
+ $collection = new RouteCollection();
+ $collection->add('known', new Route('/admin/entity/whatever', [], [], ['_admin_route' => TRUE]));
+ $collection->add('with_bundle', new Route('/admin/entity/{bundle}'));
+ $event = new RouteBuildEvent($collection);
+
+ $expected = [
+ 'known' => new Route('/admin/entity/whatever', [], [], ['_admin_route' => TRUE]),
+ 'with_bundle' => new Route('/admin/entity/{bundle}'),
+ 'entity.entity_view_display.with_field_ui_route.layout_builder' => new Route(
+ '/admin/entity/whatever/display-layout/{view_mode_name}',
+ [
+ 'entity_type_id' => 'with_field_ui_route',
+ 'bundle' => 'with_field_ui_route',
+ 'section_storage_type' => 'defaults',
+ 'section_storage' => '',
+ 'is_rebuilding' => FALSE,
+ '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::layout',
+ '_title_callback' => '\Drupal\layout_builder\Controller\LayoutBuilderController::title',
+ ],
+ [
+ '_field_ui_view_mode_access' => 'administer with_field_ui_route display',
+ '_has_layout_section' => 'true',
+ ],
+ [
+ 'parameters' => [
+ 'section_storage' => ['layout_builder_tempstore' => TRUE],
+ ],
+ '_layout_builder' => TRUE,
+ '_admin_route' => FALSE,
+ ]
+ ),
+ 'entity.entity_view_display.with_field_ui_route.layout_builder_save' => new Route(
+ '/admin/entity/whatever/display-layout/{view_mode_name}/save',
+ [
+ 'entity_type_id' => 'with_field_ui_route',
+ 'bundle' => 'with_field_ui_route',
+ 'section_storage_type' => 'defaults',
+ 'section_storage' => '',
+ '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::saveLayout',
+ ],
+ [
+ '_field_ui_view_mode_access' => 'administer with_field_ui_route display',
+ '_has_layout_section' => 'true',
+ ],
+ [
+ 'parameters' => [
+ 'section_storage' => ['layout_builder_tempstore' => TRUE],
+ ],
+ '_layout_builder' => TRUE,
+ '_admin_route' => FALSE,
+ ]
+ ),
+ 'entity.entity_view_display.with_field_ui_route.layout_builder_cancel' => new Route(
+ '/admin/entity/whatever/display-layout/{view_mode_name}/cancel',
+ [
+ 'entity_type_id' => 'with_field_ui_route',
+ 'bundle' => 'with_field_ui_route',
+ 'section_storage_type' => 'defaults',
+ 'section_storage' => '',
+ '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::cancelLayout',
+ ],
+ [
+ '_field_ui_view_mode_access' => 'administer with_field_ui_route display',
+ '_has_layout_section' => 'true',
+ ],
+ [
+ 'parameters' => [
+ 'section_storage' => ['layout_builder_tempstore' => TRUE],
+ ],
+ '_layout_builder' => TRUE,
+ '_admin_route' => FALSE,
+ ]
+ ),
+ 'entity.entity_view_display.with_bundle_key.layout_builder' => new Route(
+ '/admin/entity/whatever/display-layout/{view_mode_name}',
+ [
+ 'entity_type_id' => 'with_bundle_key',
+ 'bundle_key' => 'my_bundle_type',
+ 'section_storage_type' => 'defaults',
+ 'section_storage' => '',
+ 'is_rebuilding' => FALSE,
+ '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::layout',
+ '_title_callback' => '\Drupal\layout_builder\Controller\LayoutBuilderController::title',
+ ],
+ [
+ '_field_ui_view_mode_access' => 'administer with_bundle_key display',
+ '_has_layout_section' => 'true',
+ ],
+ [
+ 'parameters' => [
+ 'section_storage' => ['layout_builder_tempstore' => TRUE],
+ ],
+ '_layout_builder' => TRUE,
+ '_admin_route' => FALSE,
+ ]
+ ),
+ 'entity.entity_view_display.with_bundle_key.layout_builder_save' => new Route(
+ '/admin/entity/whatever/display-layout/{view_mode_name}/save',
+ [
+ 'entity_type_id' => 'with_bundle_key',
+ 'bundle_key' => 'my_bundle_type',
+ 'section_storage_type' => 'defaults',
+ 'section_storage' => '',
+ '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::saveLayout',
+ ],
+ [
+ '_field_ui_view_mode_access' => 'administer with_bundle_key display',
+ '_has_layout_section' => 'true',
+ ],
+ [
+ 'parameters' => [
+ 'section_storage' => ['layout_builder_tempstore' => TRUE],
+ ],
+ '_layout_builder' => TRUE,
+ '_admin_route' => FALSE,
+ ]
+ ),
+ 'entity.entity_view_display.with_bundle_key.layout_builder_cancel' => new Route(
+ '/admin/entity/whatever/display-layout/{view_mode_name}/cancel',
+ [
+ 'entity_type_id' => 'with_bundle_key',
+ 'bundle_key' => 'my_bundle_type',
+ 'section_storage_type' => 'defaults',
+ 'section_storage' => '',
+ '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::cancelLayout',
+ ],
+ [
+ '_field_ui_view_mode_access' => 'administer with_bundle_key display',
+ '_has_layout_section' => 'true',
+ ],
+ [
+ 'parameters' => [
+ 'section_storage' => ['layout_builder_tempstore' => TRUE],
+ ],
+ '_layout_builder' => TRUE,
+ '_admin_route' => FALSE,
+ ]
+ ),
+ 'entity.entity_view_display.with_bundle_parameter.layout_builder' => new Route(
+ '/admin/entity/{bundle}/display-layout/{view_mode_name}',
+ [
+ 'entity_type_id' => 'with_bundle_parameter',
+ 'section_storage_type' => 'defaults',
+ 'section_storage' => '',
+ 'is_rebuilding' => FALSE,
+ '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::layout',
+ '_title_callback' => '\Drupal\layout_builder\Controller\LayoutBuilderController::title',
+ ],
+ [
+ '_field_ui_view_mode_access' => 'administer with_bundle_parameter display',
+ '_has_layout_section' => 'true',
+ ],
+ [
+ 'parameters' => [
+ 'section_storage' => ['layout_builder_tempstore' => TRUE],
+ ],
+ '_layout_builder' => TRUE,
+ '_admin_route' => FALSE,
+ ]
+ ),
+ 'entity.entity_view_display.with_bundle_parameter.layout_builder_save' => new Route(
+ '/admin/entity/{bundle}/display-layout/{view_mode_name}/save',
+ [
+ 'entity_type_id' => 'with_bundle_parameter',
+ 'section_storage_type' => 'defaults',
+ 'section_storage' => '',
+ '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::saveLayout',
+ ],
+ [
+ '_field_ui_view_mode_access' => 'administer with_bundle_parameter display',
+ '_has_layout_section' => 'true',
+ ],
+ [
+ 'parameters' => [
+ 'section_storage' => ['layout_builder_tempstore' => TRUE],
+ ],
+ '_layout_builder' => TRUE,
+ '_admin_route' => FALSE,
+ ]
+ ),
+ 'entity.entity_view_display.with_bundle_parameter.layout_builder_cancel' => new Route(
+ '/admin/entity/{bundle}/display-layout/{view_mode_name}/cancel',
+ [
+ 'entity_type_id' => 'with_bundle_parameter',
+ 'section_storage_type' => 'defaults',
+ 'section_storage' => '',
+ '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::cancelLayout',
+ ],
+ [
+ '_field_ui_view_mode_access' => 'administer with_bundle_parameter display',
+ '_has_layout_section' => 'true',
+ ],
+ [
+ 'parameters' => [
+ 'section_storage' => ['layout_builder_tempstore' => TRUE],
+ ],
+ '_layout_builder' => TRUE,
+ '_admin_route' => FALSE,
+ ]
+ ),
+ ];
+
+ $this->routeBuilder->onAlterRoutes($event);
+ $this->assertEquals($expected, $event->getRouteCollection()->all());
+ }
+
}
diff --git a/core/modules/layout_builder/tests/src/Unit/LayoutTempstoreRepositoryTest.php b/core/modules/layout_builder/tests/src/Unit/LayoutTempstoreRepositoryTest.php
index b7702c1..5d1f691 100644
--- a/core/modules/layout_builder/tests/src/Unit/LayoutTempstoreRepositoryTest.php
+++ b/core/modules/layout_builder/tests/src/Unit/LayoutTempstoreRepositoryTest.php
@@ -3,6 +3,7 @@
namespace Drupal\Tests\layout_builder\Unit;
use Drupal\layout_builder\LayoutTempstoreRepository;
+use Drupal\layout_builder\Section;
use Drupal\layout_builder\SectionStorageInterface;
use Drupal\Tests\UnitTestCase;
use Drupal\Core\TempStore\SharedTempStore;
@@ -18,61 +19,133 @@ class LayoutTempstoreRepositoryTest extends UnitTestCase {
* @covers ::get
*/
public function testGetEmptyTempstore() {
- $section_storage = $this->prophesize(SectionStorageInterface::class);
- $section_storage->getStorageType()->willReturn('my_storage_type');
- $section_storage->getStorageId()->willReturn('my_storage_id');
+ $section_storage = new TestSectionStorage();
$tempstore = $this->prophesize(SharedTempStore::class);
$tempstore->get('my_storage_id')->shouldBeCalled();
$tempstore_factory = $this->prophesize(SharedTempStoreFactory::class);
- $tempstore_factory->get('layout_builder.my_storage_type')->willReturn($tempstore->reveal());
+ $tempstore_factory->get('layout_builder.section_storage.my_storage_type')->willReturn($tempstore->reveal());
$repository = new LayoutTempstoreRepository($tempstore_factory->reveal());
- $result = $repository->get($section_storage->reveal());
- $this->assertSame($section_storage->reveal(), $result);
+ $result = $repository->get($section_storage);
+ $this->assertSame($section_storage, $result);
}
/**
* @covers ::get
*/
public function testGetLoadedTempstore() {
- $section_storage = $this->prophesize(SectionStorageInterface::class);
- $section_storage->getStorageType()->willReturn('my_storage_type');
- $section_storage->getStorageId()->willReturn('my_storage_id');
+ $section_storage = new TestSectionStorage();
- $tempstore_section_storage = $this->prophesize(SectionStorageInterface::class);
+ $tempstore_section_storage = new TestSectionStorage();
$tempstore = $this->prophesize(SharedTempStore::class);
- $tempstore->get('my_storage_id')->willReturn(['section_storage' => $tempstore_section_storage->reveal()]);
+ $tempstore->get('my_storage_id')->willReturn(['section_storage' => $tempstore_section_storage]);
$tempstore_factory = $this->prophesize(SharedTempStoreFactory::class);
- $tempstore_factory->get('layout_builder.my_storage_type')->willReturn($tempstore->reveal());
+ $tempstore_factory->get('layout_builder.section_storage.my_storage_type')->willReturn($tempstore->reveal());
$repository = new LayoutTempstoreRepository($tempstore_factory->reveal());
- $result = $repository->get($section_storage->reveal());
- $this->assertSame($tempstore_section_storage->reveal(), $result);
- $this->assertNotSame($section_storage->reveal(), $result);
+ $result = $repository->get($section_storage);
+ $this->assertSame($tempstore_section_storage, $result);
+ $this->assertNotSame($section_storage, $result);
}
/**
* @covers ::get
*/
public function testGetInvalidEntry() {
- $section_storage = $this->prophesize(SectionStorageInterface::class);
- $section_storage->getStorageType()->willReturn('my_storage_type');
- $section_storage->getStorageId()->willReturn('my_storage_id');
+ $section_storage = new TestSectionStorage();
$tempstore = $this->prophesize(SharedTempStore::class);
$tempstore->get('my_storage_id')->willReturn(['section_storage' => 'this_is_not_an_entity']);
$tempstore_factory = $this->prophesize(SharedTempStoreFactory::class);
- $tempstore_factory->get('layout_builder.my_storage_type')->willReturn($tempstore->reveal());
+ $tempstore_factory->get('layout_builder.section_storage.my_storage_type')->willReturn($tempstore->reveal());
$repository = new LayoutTempstoreRepository($tempstore_factory->reveal());
$this->setExpectedException(\UnexpectedValueException::class, 'The entry with storage type "my_storage_type" and ID "my_storage_id" is invalid');
- $repository->get($section_storage->reveal());
+ $repository->get($section_storage);
}
}
+
+/**
+ * Provides a test implementation of section storage.
+ *
+ * @todo This works around https://github.com/phpspec/prophecy/issues/119.
+ */
+class TestSectionStorage implements SectionStorageInterface {
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getStorageType() {
+ return 'my_storage_type';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getStorageId() {
+ return 'my_storage_id';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function count() {}
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSections() {}
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSection($delta) {}
+
+ /**
+ * {@inheritdoc}
+ */
+ public function appendSection(Section $section) {}
+
+ /**
+ * {@inheritdoc}
+ */
+ public function insertSection($delta, Section $section) {}
+
+ /**
+ * {@inheritdoc}
+ */
+ public function removeSection($delta) {}
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getContexts() {}
+
+ /**
+ * {@inheritdoc}
+ */
+ public function label() {}
+
+ /**
+ * {@inheritdoc}
+ */
+ public function save() {}
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCanonicalUrl() {}
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getLayoutBuilderUrl() {}
+
+}
diff --git a/core/modules/layout_builder/tests/src/Unit/SectionStorageDefaultsParamConverterTest.php b/core/modules/layout_builder/tests/src/Unit/SectionStorageDefaultsParamConverterTest.php
new file mode 100644
index 0000000..8afde9a
--- /dev/null
+++ b/core/modules/layout_builder/tests/src/Unit/SectionStorageDefaultsParamConverterTest.php
@@ -0,0 +1,134 @@
+<?php
+
+namespace Drupal\Tests\layout_builder\Unit;
+
+use Drupal\Core\Entity\EntityManagerInterface;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Entity\EntityType;
+use Drupal\layout_builder\Routing\SectionStorageDefaultsParamConverter;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\layout_builder\Routing\SectionStorageDefaultsParamConverter
+ *
+ * @group layout_builder
+ */
+class SectionStorageDefaultsParamConverterTest extends UnitTestCase {
+
+ /**
+ * The converter.
+ *
+ * @var \Drupal\layout_builder\Routing\SectionStorageDefaultsParamConverter
+ */
+ protected $converter;
+
+ /**
+ * The entity manager.
+ *
+ * @var \Drupal\Core\Entity\EntityManagerInterface
+ */
+ protected $entityManager;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp() {
+ parent::setUp();
+
+ $this->entityManager = $this->prophesize(EntityManagerInterface::class);
+ $this->converter = new SectionStorageDefaultsParamConverter($this->entityManager->reveal());
+ }
+
+ /**
+ * @covers ::convert
+ * @covers ::getEntityTypeFromDefaults
+ *
+ * @dataProvider providerTestConvert
+ */
+ public function testConvert($success, $expected_entity_id, $value, array $defaults) {
+ if ($expected_entity_id) {
+ $entity_storage = $this->prophesize(EntityStorageInterface::class);
+ $entity_storage->load($expected_entity_id)->willReturn('the_return_value');
+
+ $this->entityManager->getDefinition('entity_view_display')->willReturn(new EntityType(['id' => 'entity_view_display']));
+ $this->entityManager->getStorage('entity_view_display')->willReturn($entity_storage->reveal());
+ }
+ else {
+ $this->entityManager->getDefinition('entity_view_display')->shouldNotBeCalled();
+ $this->entityManager->getStorage('entity_view_display')->shouldNotBeCalled();
+ }
+
+ $result = $this->converter->convert($value, [], 'the_parameter_name', $defaults);
+ if ($success) {
+ $this->assertEquals('the_return_value', $result);
+ }
+ else {
+ $this->assertNull($result);
+ }
+ }
+
+ /**
+ * Provides data for ::testConvert().
+ */
+ public function providerTestConvert() {
+ $data = [];
+ $data['with value'] = [
+ TRUE,
+ 'some_value',
+ 'some_value',
+ [],
+ ];
+ $data['empty value, without bundle'] = [
+ TRUE,
+ 'my_entity_type.bundle_name.default',
+ '',
+ [
+ 'entity_type_id' => 'my_entity_type',
+ 'view_mode_name' => 'default',
+ 'bundle_key' => 'my_bundle',
+ 'my_bundle' => 'bundle_name',
+ ],
+ ];
+ $data['empty value, with bundle'] = [
+ TRUE,
+ 'my_entity_type.bundle_name.default',
+ '',
+ [
+ 'entity_type_id' => 'my_entity_type',
+ 'view_mode_name' => 'default',
+ 'bundle' => 'bundle_name',
+ ],
+ ];
+ $data['without value, empty defaults'] = [
+ FALSE,
+ NULL,
+ '',
+ [],
+ ];
+ return $data;
+ }
+
+ /**
+ * @covers ::convert
+ */
+ public function testConvertCreate() {
+ $expected = 'the_return_value';
+ $value = 'foo.bar.baz';
+ $expected_create_values = [
+ 'targetEntityType' => 'foo',
+ 'bundle' => 'bar',
+ 'mode' => 'baz',
+ 'status' => TRUE,
+ ];
+ $entity_storage = $this->prophesize(EntityStorageInterface::class);
+ $entity_storage->load($value)->willReturn(NULL);
+ $entity_storage->create($expected_create_values)->willReturn($expected);
+
+ $this->entityManager->getDefinition('entity_view_display')->willReturn(new EntityType(['id' => 'entity_view_display']));
+ $this->entityManager->getStorage('entity_view_display')->willReturn($entity_storage->reveal());
+
+ $result = $this->converter->convert($value, [], 'the_parameter_name', []);
+ $this->assertSame($expected, $result);
+ }
+
+}