diff --git a/core/config/schema/core.entity.schema.yml b/core/config/schema/core.entity.schema.yml index 73d84bc4dfce736c26a3e7379babba21a93c4647..57e55472555e8958abcda3a67c2c5b53e8b6cd55 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 b870007e33588738af1959f0fca25e93debf0ad6..682caa78c45e72842f35c7a4d253b13cf8927fef 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 e9859114643d4d4e361dacfc53f75ab35cb983da..d4ce72e0ffaf8d564b6d6222b089b6b3e2c9ce7e 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 0000000000000000000000000000000000000000..acb1e4fdf3d9cc6bdcf3a5e39a95e89557fe9c48 --- /dev/null +++ b/core/modules/layout_builder/layout_builder.install @@ -0,0 +1,40 @@ +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 895145d984e896c1db0b16b2fe06b1f0a71801e7..80339c1f70e591efaa20d4e3aac5c9808f64b13c 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 db6a1c13b36882c17cdef9436028940c04c9a286..38933073d965a097cb4b2d57ad33e5357d8cb86b 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 c632f4b33a0d439afbefe092428de54facf09bef..3c3bc25c400585e5ee2d09365fdb7dae69c7dd6e 100644 --- a/core/modules/layout_builder/src/Cache/LayoutBuilderIsActiveCacheContext.php +++ b/core/modules/layout_builder/src/Cache/LayoutBuilderIsActiveCacheContext.php @@ -4,8 +4,8 @@ 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 @@ public function getContext($entity_type_id = NULL) { } $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 @@ public function getCacheableMetadata($entity_type_id = NULL) { * * @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 5ccddfac38aad248a57225142861d9bd81c39c77..9642c4e2a70a3d17070dbe5048e78fd4dced384e 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; @@ -30,14 +32,24 @@ 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 @@ public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore */ 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 @@ public function layout(SectionStorageInterface $section_storage, $is_rebuilding * 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 @@ protected function buildAdministrativeSection(SectionStorageInterface $section_s $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 @@ protected function buildAdministrativeSection(SectionStorageInterface $section_s 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 @@ public function saveLayout(SectionStorageInterface $section_storage) { */ 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 0000000000000000000000000000000000000000..a612eb5a587052584b8639dc5b90abe6ece15a16 --- /dev/null +++ b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php @@ -0,0 +1,488 @@ +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 0000000000000000000000000000000000000000..e86df9daf840173346ee4fcbe86ea6e8182f19e0 --- /dev/null +++ b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplayStorage.php @@ -0,0 +1,60 @@ +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 0000000000000000000000000000000000000000..1affc53b399418fe6edf519ff6fdde0a0b2a535d --- /dev/null +++ b/core/modules/layout_builder/src/Entity/LayoutEntityDisplayInterface.php @@ -0,0 +1,36 @@ +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 @@ public function getContexts() { /** * {@inheritdoc} */ - public function getStorageType() { + public static function getStorageType() { return 'overrides'; } @@ -131,6 +134,13 @@ public function getLayoutBuilderUrl() { return $this->getEntity()->toUrl('layout-builder'); } + /** + * {@inheritdoc} + */ + public function getDefaultSectionStorage() { + return LayoutBuilderEntityViewDisplay::collectRenderDisplay($this->getEntity(), 'default'); + } + /** * {@inheritdoc} */ diff --git a/core/modules/layout_builder/src/Form/AddBlockForm.php b/core/modules/layout_builder/src/Form/AddBlockForm.php index 83effd622661ae357b70a8122afd3192f0df89e7..704d136ba96fc03d0b22520c16e40c6cb707a232 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 @@ protected function submitLabel() { } /** - * {@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 b75e88f0838b595c856f0033992efeb7c11edf9c..e1103e9e44bbce830144475fad98074f789604c6 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\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. @@ -82,6 +82,13 @@ abstract class ConfigureBlockFormBase extends FormBase { */ protected $region; + /** + * The UUID of the component. + * + * @var string + */ + protected $uuid; + /** * The section storage. * @@ -109,7 +116,7 @@ public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore $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; } @@ -128,25 +135,6 @@ public static function create(ContainerInterface $container) { ); } - /** - * 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. * @@ -158,21 +146,17 @@ protected function prepareBlock($block_id, array $configuration) { * 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)); @@ -204,20 +188,6 @@ public function buildForm(array $form, FormStateInterface $form_state, SectionSt */ 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} */ @@ -242,7 +212,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) { $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 0000000000000000000000000000000000000000..9fa20648ff27c6f8545da292c716a685a00a337c --- /dev/null +++ b/core/modules/layout_builder/src/Form/LayoutBuilderEntityViewDisplayForm.php @@ -0,0 +1,94 @@ + '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 0000000000000000000000000000000000000000..b6d07d90890c39ea1dc8bd6747fe4a1ac776b761 --- /dev/null +++ b/core/modules/layout_builder/src/Form/RevertOverridesForm.php @@ -0,0 +1,117 @@ +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 afca0d25c975f14a07454c645406eb4b0373239c..c00b406eb2cb2a5e666410fd185ad408cf2659cd 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 @@ public function getFormId() { * 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 @@ protected function submitLabel() { 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 0fa9d738bf0c9720868075b032aad93cf284b496..39725afc7e20068e9d25fdea0af447d0c32c117f 100644 --- a/core/modules/layout_builder/src/LayoutTempstoreRepository.php +++ b/core/modules/layout_builder/src/LayoutTempstoreRepository.php @@ -71,7 +71,7 @@ public function delete(SectionStorageInterface $section_storage) { * 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 0000000000000000000000000000000000000000..0d6fd2b05645cb74f7619b4633dd9947b10b26dd --- /dev/null +++ b/core/modules/layout_builder/src/OverridesSectionStorageInterface.php @@ -0,0 +1,26 @@ +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 @@ protected function getEntity() { */ 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 @@ public function blockSubmit($form, FormStateInterface $form_state) { */ 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 6a39f4a17c4c28bac7d2fbc9aab2d49d13e1b394..71b9c1e316c69cfd7ec62db04aa5912403023578 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\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 @@ public static function create(ContainerInterface $container, $base_plugin_id) { 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 0aacc0d052889cd810ba8c73e6f327ffa26bb67c..94160a7736f5d3b1f31cdcfb0cb0663fde74a850 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 @@ /** * 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 @@ public static function create(ContainerInterface $container, $base_plugin_id) { * {@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 @@ public function getDerivativeDefinitions($base_plugin_definition) { '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 563021ed57cac4f832d5459e89e95f225757ebdd..500a380a11bed717b2a7be6f30fc2ccbe1c03b0a 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 @@ public function getRoutes() { $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 @@ protected function getEntityTypes() { }); } + /** + * {@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 0000000000000000000000000000000000000000..da9e1c1759f15ee632978856c97543778c2f0047 --- /dev/null +++ b/core/modules/layout_builder/src/Routing/LayoutBuilderRoutesTrait.php @@ -0,0 +1,92 @@ +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 0000000000000000000000000000000000000000..2f886102c3e7f8786530ca032ce930711789378d --- /dev/null +++ b/core/modules/layout_builder/src/Routing/SectionStorageDefaultsParamConverter.php @@ -0,0 +1,53 @@ +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 02f274e784265c8e0fd82e670741337c88fe72b1..04b1cbb2c60eccecec18650a60d4e0382ab2a25d 100644 --- a/core/modules/layout_builder/src/Section.php +++ b/core/modules/layout_builder/src/Section.php @@ -68,14 +68,16 @@ public function __construct($layout_id, array $layout_settings = [], array $comp * * @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; } } @@ -132,6 +134,16 @@ public function setLayoutSettings(array $layout_settings) { return $this; } + /** + * 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. * @@ -307,4 +319,23 @@ protected function layoutPluginManager() { 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 6a0c238c7044592e4a3149021acd37b9ac4136d2..7af03af132b21f9b15074ffcb86ab392a2d3730d 100644 --- a/core/modules/layout_builder/src/SectionComponent.php +++ b/core/modules/layout_builder/src/SectionComponent.php @@ -90,11 +90,13 @@ public function __construct($uuid, $region, array $configuration = [], array $ad * * @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 @@ public function toRenderArray(array $contexts = []) { $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 @@ public function setConfiguration(array $configuration) { * @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 @@ protected function currentUser() { 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 13217d82b40d2008ed3c9c76ef767b606ca31caa..cc1efaba2a62586a318e804c7184f8e38095d5aa 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 @@ public function insertSection($delta, Section $section); /** * 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 @@ public function getStorageId(); * @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 0000000000000000000000000000000000000000..63c084cf9825a9f6f4dc7f4bf6e8609381d9a0b9 --- /dev/null +++ b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php @@ -0,0 +1,232 @@ +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 b2b5f9c7818a3fc326b1e341ed9c3a5ce7e4fb88..e0dfdc7f429d16b99c74b63994099715da2769d0 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 @@ protected function setUp() { 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 @@ public function providerTestLayoutSectionFormatter() { [ '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', ], @@ -276,6 +278,46 @@ public function testLayoutUrlNoSectionField() { $this->assertSession()->statusCodeEquals(404); } + /** + * 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. * diff --git a/core/modules/layout_builder/tests/src/FunctionalJavascript/FieldBlockTest.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/FieldBlockTest.php index e7ae850d42bbf5f35b425a76888278ffd2dfd11f..4f086d6a96b966a793a1ca4211b700e15f8fe2f2 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 @@ public function testFieldBlock() { $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 @@ public function testFieldBlock() { ]; $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 a118cf5bef8a99e351d4449826878a6f7e120c24..8c529cc1e1b93111dc82d2e1c770470714473c71 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 @@ protected function getTestBlock(ProphecyInterface $entity_prophecy, array $confi $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 6c376fa7cf4babd704985723effd0d36fabd99c0..d5a4cd0b8a8fd7c937b39d8eccf02f95b5433d20 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 @@ protected function setUp() { $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 @@ protected function setUp() { '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 @@ protected function installLayoutBuilder() { $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 0000000000000000000000000000000000000000..034a76628e3cec5cb566cb212e15583745f257d0 --- /dev/null +++ b/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderEntityViewDisplayTest.php @@ -0,0 +1,51 @@ + '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 3873af81b022aba795cf651d5f3f4bbb3dd535a9..fa646df1c91b10564a04df86110ec6e9fcd80d20 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 @@ public function testCompatibility() { $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 f3a6e6f950e9dd11f33eceec9801f85e3cfe21c6..af8395f1731a40da12371a7e36bc33a6f579eb15 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 97c1fba713eb3572c9c16edc6906c1ffbfd0b4d9..151cf736f974f27e8d74fbc53f2434f76bc9e625 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 protected function getSectionStorage(array $section_data); 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 @@ public function testGetSectionInvalidDelta() { 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 @@ public function testInsertSection() { 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 @@ public function testAppendSection() { 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 e209f6ad43e86ddb0637b12ea0be4847a66b4bf0..961b870429880582cbc30b44100550922ddc5e7f 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\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 @@ public function testGetRoutes() { '_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 @@ public function testGetRoutes() { '_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 @@ public function testGetRoutes() { '_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 @@ public function testGetRoutes() { '_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 @@ public function testGetRoutes() { '_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 b7702c118424f99f9ddb4384b9c7676dd1fd2426..5d1f691271d5b3280cdbe868089df9b51e2a6f25 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 0000000000000000000000000000000000000000..8afde9a05558e91c18e0d3be5976666b30d97181 --- /dev/null +++ b/core/modules/layout_builder/tests/src/Unit/SectionStorageDefaultsParamConverterTest.php @@ -0,0 +1,134 @@ +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); + } + +}