diff --git a/core/modules/layout_builder/layout_builder.links.contextual.yml b/core/modules/layout_builder/layout_builder.links.contextual.yml index 67952a1f18..bcf2a9cf06 100644 --- a/core/modules/layout_builder/layout_builder.links.contextual.yml +++ b/core/modules/layout_builder/layout_builder.links.contextual.yml @@ -1,6 +1,6 @@ -layout_builder_block_configure: +layout_builder_block_update: title: 'Configure' - route_name: 'layout_builder.configure_block' + route_name: 'layout_builder.update_block' group: 'layout_builder_block' options: attributes: diff --git a/core/modules/layout_builder/layout_builder.module b/core/modules/layout_builder/layout_builder.module index e8d8a1116a..74367cd728 100644 --- a/core/modules/layout_builder/layout_builder.module +++ b/core/modules/layout_builder/layout_builder.module @@ -6,6 +6,7 @@ */ use Drupal\Core\Entity\Display\EntityViewDisplayInterface; +use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; @@ -23,6 +24,18 @@ function layout_builder_help($route_name) { } } +/** + * Implements hook_entity_type_alter(). + */ +function layout_builder_entity_type_alter(array &$entity_types) { + /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */ + foreach ($entity_types as $entity_type) { + if ($entity_type->entityClassImplements(FieldableEntityInterface::class) && $entity_type->hasLinkTemplate('canonical') && $entity_type->hasViewBuilderClass()) { + $entity_type->setLinkTemplate('layout-builder', $entity_type->getLinkTemplate('canonical') . '/layout'); + } + } +} + /** * Implements hook_form_FORM_ID_alter() for \Drupal\field_ui\Form\EntityViewDisplayEditForm. */ @@ -53,21 +66,22 @@ function layout_builder_form_entity_view_display_edit_form_alter(&$form, FormSta '#default_value' => $display->getThirdPartySetting('layout_builder', 'allow_custom', FALSE), ]; - // The submit handler should run before the entity is saved by the form. - array_unshift($form['actions']['submit']['#submit'], 'layout_builder_form_entity_view_display_edit_submit'); + $form['#entity_builders'][] = 'layout_builder_form_entity_view_display_edit_entity_builder'; } /** - * Form submission handler for layout options on the entity view display form. + * 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_submit(&$form, FormStateInterface $form_state) { - /** @var \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display */ - $display = $form_state->getFormObject()->getEntity(); +function layout_builder_form_entity_view_display_edit_entity_builder($entity_type_id, EntityViewDisplayInterface $display, &$form, FormStateInterface &$form_state) { + // Only proceed once the form has been submitted. + if (!$form_state->isSubmitted()) { + return; + } - $original_value = $display->getThirdPartySetting('layout_builder', 'allow_custom', FALSE); - $allow_custom = $form_state->getValue(['layout', 'allow_custom'], FALSE); + $original_value = (bool) $display->getThirdPartySetting('layout_builder', 'allow_custom', FALSE); + $allow_custom = (bool) $form_state->getValue(['layout', 'allow_custom'], FALSE); // Only continue if the value has changed. if ($original_value !== $allow_custom) { diff --git a/core/modules/layout_builder/layout_builder.routing.yml b/core/modules/layout_builder/layout_builder.routing.yml index 27a234a7be..8fe952afbe 100644 --- a/core/modules/layout_builder/layout_builder.routing.yml +++ b/core/modules/layout_builder/layout_builder.routing.yml @@ -70,7 +70,7 @@ layout_builder.choose_block: layout_builder.add_block: path: '/layout_builder/add/block/{entity_type_id}/{entity}/{delta}/{region}/{plugin_id}' defaults: - _form: '\Drupal\layout_builder\Form\ConfigureBlockForm' + _form: '\Drupal\layout_builder\Form\AddBlockForm' requirements: _permission: 'configure any layout' options: @@ -80,10 +80,10 @@ layout_builder.add_block: type: entity:{entity_type_id} layout_builder_tempstore: TRUE -layout_builder.configure_block: - path: '/layout_builder/configure/block/{entity_type_id}/{entity}/{delta}/{region}/{uuid}' +layout_builder.update_block: + path: '/layout_builder/update/block/{entity_type_id}/{entity}/{delta}/{region}/{uuid}' defaults: - _form: '\Drupal\layout_builder\Form\ConfigureBlockForm' + _form: '\Drupal\layout_builder\Form\UpdateBlockForm' requirements: _permission: 'configure any layout' options: diff --git a/core/modules/layout_builder/layout_builder.services.yml b/core/modules/layout_builder/layout_builder.services.yml index a717a020a7..575dbc75ad 100644 --- a/core/modules/layout_builder/layout_builder.services.yml +++ b/core/modules/layout_builder/layout_builder.services.yml @@ -12,7 +12,7 @@ services: - { name: access_check, applies_to: _has_layout_section } layout_builder.routes: class: Drupal\layout_builder\Routing\LayoutBuilderRoutes - arguments: ['@entity_type.manager'] + arguments: ['@entity_type.manager', '@entity_field.manager'] layout_builder.route_enhancer: class: Drupal\layout_builder\Routing\LayoutBuilderRouteEnhancer arguments: ['@entity_type.manager'] diff --git a/core/modules/layout_builder/src/Controller/AddSectionController.php b/core/modules/layout_builder/src/Controller/AddSectionController.php index 5dcf2ef208..5e1f3536aa 100644 --- a/core/modules/layout_builder/src/Controller/AddSectionController.php +++ b/core/modules/layout_builder/src/Controller/AddSectionController.php @@ -83,7 +83,7 @@ public function build(EntityInterface $entity, $delta, $plugin_id) { return $this->rebuildAndClose($entity); } else { - $url = Url::fromRoute("entity.{$entity->getEntityTypeId()}.layout", [$entity->getEntityTypeId() => $entity->id()]); + $url = $entity->toUrl('layout-builder'); return new RedirectResponse($url->setAbsolute()->toString()); } } diff --git a/core/modules/layout_builder/src/Controller/LayoutBuilderController.php b/core/modules/layout_builder/src/Controller/LayoutBuilderController.php index 13f1582944..f121814461 100644 --- a/core/modules/layout_builder/src/Controller/LayoutBuilderController.php +++ b/core/modules/layout_builder/src/Controller/LayoutBuilderController.php @@ -10,7 +10,7 @@ use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\Url; use Drupal\layout_builder\LayoutSectionBuilder; -use Drupal\layout_builder\LayoutSectionItemInterface; +use Drupal\layout_builder\Field\LayoutSectionItemInterface; use Drupal\layout_builder\LayoutTempstoreRepositoryInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\RedirectResponse; @@ -111,7 +111,7 @@ public function layout(EntityInterface $entity) { $output = []; $count = 0; - /** @var \Drupal\layout_builder\LayoutSectionItemInterface $item */ + /** @var \Drupal\layout_builder\Field\LayoutSectionItemInterface $item */ foreach ($entity->layout_builder__layout as $item) { $output[] = $this->buildAddSectionLink($entity_type_id, $entity_id, $count); $output[] = $this->buildAdministrativeSection($item, $entity, $count); @@ -169,7 +169,7 @@ protected function buildAddSectionLink($entity_type_id, $entity_id, $delta) { /** * Builds the render array for the layout section while editing. * - * @param \Drupal\layout_builder\LayoutSectionItemInterface $item + * @param \Drupal\layout_builder\Field\LayoutSectionItemInterface $item * The layout section item. * @param \Drupal\Core\Entity\EntityInterface $entity * The entity. diff --git a/core/modules/layout_builder/src/Controller/MoveBlockController.php b/core/modules/layout_builder/src/Controller/MoveBlockController.php index e1be964bad..800223eef6 100644 --- a/core/modules/layout_builder/src/Controller/MoveBlockController.php +++ b/core/modules/layout_builder/src/Controller/MoveBlockController.php @@ -59,14 +59,24 @@ public static function create(ContainerInterface $container) { * * @param \Drupal\Core\Entity\EntityInterface $entity * The entity. - * @param \Symfony\Component\HttpFoundation\Request $request - * The request. + * @param int $delta_from + * The delta of the original section. + * @param int $delta_to + * The delta of the destination section. + * @param string $region_from + * The original region for this block. + * @param string $region_to + * The new region for this block. + * @param string $block_uuid + * The UUID for this block. + * @param string|null $preceding_block_uuid + * (optional) If provided, the UUID of the block to insert this block after. * * @return \Drupal\Core\Ajax\AjaxResponse * An AJAX response. */ - public function build(EntityInterface $entity, Request $request, $delta_from, $delta_to, $region_from, $region_to, $block_uuid, $preceding_block_uuid = NULL) { - /** @var \Drupal\layout_builder\LayoutSectionItemInterface $field */ + public function build(EntityInterface $entity, $delta_from, $delta_to, $region_from, $region_to, $block_uuid, $preceding_block_uuid = NULL) { + /** @var \Drupal\layout_builder\Field\LayoutSectionItemInterface $field */ $field = $entity->layout_builder__layout->get($delta_from); $values = $field->section; @@ -74,9 +84,10 @@ public function build(EntityInterface $entity, Request $request, $delta_from, $d unset($values[$region_from][$block_uuid]); $field->section = array_filter($values); - /** @var \Drupal\layout_builder\LayoutSectionItemInterface $field */ - $field = $entity->layout_builder__layout->get($delta_to); - $values = $field->section; + if ($delta_from !== $delta_to) { + $field = $entity->layout_builder__layout->get($delta_to); + $values = $field->section; + } if (isset($preceding_block_uuid)) { if (!isset($values[$region_to])) { @@ -85,7 +96,7 @@ public function build(EntityInterface $entity, Request $request, $delta_from, $d $slice_id = array_search($preceding_block_uuid, array_keys($values[$region_to])); if ($slice_id === FALSE) { - throw new \InvalidArgumentException('Invalid preceeding block UUID'); + throw new \InvalidArgumentException('Invalid preceding block UUID'); } $before = array_slice($values[$region_to], 0, $slice_id + 1); @@ -93,9 +104,7 @@ public function build(EntityInterface $entity, Request $request, $delta_from, $d $values[$region_to] = array_merge($before, [$block_uuid => $configuration], $after); } else { - if (empty($values[$region_to])) { - $values[$region_to] = []; - } + $values += [$region_to => []]; $values[$region_to] = array_merge([$block_uuid => $configuration], $values[$region_to]); } $field->section = array_filter($values); diff --git a/core/modules/layout_builder/src/LayoutSectionItemInterface.php b/core/modules/layout_builder/src/Field/LayoutSectionItemInterface.php similarity index 50% rename from core/modules/layout_builder/src/LayoutSectionItemInterface.php rename to core/modules/layout_builder/src/Field/LayoutSectionItemInterface.php index 48e081a821..c2288608b0 100644 --- a/core/modules/layout_builder/src/LayoutSectionItemInterface.php +++ b/core/modules/layout_builder/src/Field/LayoutSectionItemInterface.php @@ -1,12 +1,17 @@ t('Add Block'); + } + +} diff --git a/core/modules/layout_builder/src/Form/ConfigureBlockForm.php b/core/modules/layout_builder/src/Form/ConfigureBlockFormBase.php similarity index 85% rename from core/modules/layout_builder/src/Form/ConfigureBlockForm.php rename to core/modules/layout_builder/src/Form/ConfigureBlockFormBase.php index 1144a73e6d..a91e35478b 100644 --- a/core/modules/layout_builder/src/Form/ConfigureBlockForm.php +++ b/core/modules/layout_builder/src/Form/ConfigureBlockFormBase.php @@ -21,11 +21,11 @@ use Symfony\Component\HttpFoundation\RequestStack; /** - * Provides a form to configure a block. + * Provides a base form for configuring a block. * * @internal */ -class ConfigureBlockForm extends FormBase { +abstract class ConfigureBlockFormBase extends FormBase { use ContextAwarePluginAssignmentTrait; use LayoutRebuildFormTrait; @@ -94,7 +94,7 @@ class ConfigureBlockForm extends FormBase { protected $entity; /** - * Constructs a new ConfigureBlockForm. + * Constructs a new block form. * * @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository * The layout tempstore repository. @@ -136,13 +136,6 @@ public static function create(ContainerInterface $container) { ); } - /** - * {@inheritdoc} - */ - public function getFormId() { - return 'layout_builder_configure_block'; - } - /** * Prepares the block plugin based on the block ID. * @@ -163,26 +156,35 @@ protected function prepareBlock($block_id, array $configuration) { } /** - * {@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\Core\Entity\EntityInterface $entity + * The entity 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. + * + * @return array + * The form array. */ - public function buildForm(array $form, FormStateInterface $form_state, EntityInterface $entity = NULL, $delta = NULL, $region = NULL, $plugin_id = NULL, $uuid = NULL) { + public function buildForm(array $form, FormStateInterface $form_state, EntityInterface $entity = NULL, $delta = NULL, $region = NULL, $plugin_id = NULL, array $configuration = []) { $this->entity = $entity; $this->delta = $delta; $this->region = $region; - - $configuration = []; - if ($uuid) { - /** @var \Drupal\layout_builder\LayoutSectionItemInterface $field */ - $field = $this->entity->layout_builder__layout->get($this->delta); - $plugin_id = $field->section[$region][$uuid]['block']['id']; - $configuration = $field->section[$region][$uuid]['block']; - } $this->block = $this->prepareBlock($plugin_id, $configuration); $form_state->setTemporaryValue('gathered_contexts', $this->contextRepository->getAvailableContexts()); - // Some Block Plugins rely on the block_theme value to load theme settings. - // @see \Drupal\system\Plugin\Block\SystemBrandingBlock::blockForm(). + // @todo Remove once https://www.drupal.org/node/2268787 is resolved. $form_state->set('block_theme', $this->config('system.theme')->get('default')); $form['#tree'] = TRUE; @@ -192,7 +194,7 @@ public function buildForm(array $form, FormStateInterface $form_state, EntityInt $form['actions']['submit'] = [ '#type' => 'submit', - '#value' => $uuid ? $this->t('Update') : $this->t('Add Block'), + '#value' => $this->submitLabel(), '#button_type' => 'primary', ]; if ($this->isAjax()) { @@ -202,6 +204,14 @@ public function buildForm(array $form, FormStateInterface $form_state, EntityInt return $form; } + /** + * Returns the label for the submit button. + * + * @return string + * Submit label. + */ + abstract protected function submitLabel(); + /** * {@inheritdoc} */ @@ -225,14 +235,14 @@ public function submitForm(array &$form, FormStateInterface $form_state) { $configuration = $this->block->getConfiguration(); - /** @var \Drupal\layout_builder\LayoutSectionItemInterface $field */ + /** @var \Drupal\layout_builder\Field\LayoutSectionItemInterface $field */ $field = $this->entity->layout_builder__layout->get($this->delta); $section = $field->section; $section[$this->region][$configuration['uuid']]['block'] = $configuration; $field->section = $section; $this->layoutTempstoreRepository->set($this->entity); - $form_state->setRedirect("entity.{$this->entity->getEntityTypeId()}.layout", [$this->entity->getEntityTypeId() => $this->entity->id()]); + $form_state->setRedirectUrl($this->entity->toUrl('layout-builder')); } /** diff --git a/core/modules/layout_builder/src/Form/ConfigureSectionForm.php b/core/modules/layout_builder/src/Form/ConfigureSectionForm.php index abc1f8c2bb..ddf1c96040 100644 --- a/core/modules/layout_builder/src/Form/ConfigureSectionForm.php +++ b/core/modules/layout_builder/src/Form/ConfigureSectionForm.php @@ -127,7 +127,7 @@ public function buildForm(array $form, FormStateInterface $form_state, EntityInt $configuration = []; if ($this->isUpdate) { - /** @var \Drupal\layout_builder\LayoutSectionItemInterface $field */ + /** @var \Drupal\layout_builder\Field\LayoutSectionItemInterface $field */ $field = $this->entity->layout_builder__layout->get($this->delta); $plugin_id = $field->layout; $configuration = $field->layout_settings; @@ -186,7 +186,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) { } $this->layoutTempstoreRepository->set($this->entity); - $form_state->setRedirect("entity.{$this->entity->getEntityTypeId()}.layout", [$this->entity->getEntityTypeId() => $this->entity->id()]); + $form_state->setRedirectUrl($this->entity->toUrl('layout-builder')); } /** diff --git a/core/modules/layout_builder/src/Form/LayoutRebuildConfirmFormBase.php b/core/modules/layout_builder/src/Form/LayoutRebuildConfirmFormBase.php index b1765977ba..6214ea364b 100644 --- a/core/modules/layout_builder/src/Form/LayoutRebuildConfirmFormBase.php +++ b/core/modules/layout_builder/src/Form/LayoutRebuildConfirmFormBase.php @@ -73,7 +73,7 @@ public static function create(ContainerInterface $container) { * {@inheritdoc} */ public function getCancelUrl() { - return Url::fromRoute("entity.{$this->entity->getEntityTypeId()}.layout", [$this->entity->getEntityTypeId() => $this->entity->id()]); + return $this->entity->toUrl('layout-builder'); } /** diff --git a/core/modules/layout_builder/src/Form/RemoveBlockForm.php b/core/modules/layout_builder/src/Form/RemoveBlockForm.php index 8772de2602..0fccbb8bb9 100644 --- a/core/modules/layout_builder/src/Form/RemoveBlockForm.php +++ b/core/modules/layout_builder/src/Form/RemoveBlockForm.php @@ -60,7 +60,7 @@ public function buildForm(array $form, FormStateInterface $form_state, EntityInt * {@inheritdoc} */ protected function handleEntity(EntityInterface $entity, FormStateInterface $form_state) { - /** @var \Drupal\layout_builder\LayoutSectionItemInterface $field */ + /** @var \Drupal\layout_builder\Field\LayoutSectionItemInterface $field */ $field = $entity->layout_builder__layout->get($this->delta); $values = $field->section; unset($values[$this->region][$this->uuid]); diff --git a/core/modules/layout_builder/src/Form/UpdateBlockForm.php b/core/modules/layout_builder/src/Form/UpdateBlockForm.php new file mode 100644 index 0000000000..df9369841f --- /dev/null +++ b/core/modules/layout_builder/src/Form/UpdateBlockForm.php @@ -0,0 +1,61 @@ +layout_builder__layout->get($delta); + if (empty($field->section[$region][$uuid]['block'])) { + throw new \InvalidArgumentException('Invalid UUID specified'); + } + + $plugin_id = $field->section[$region][$uuid]['block']['id']; + $configuration = $field->section[$region][$uuid]['block']; + + return parent::buildForm($form, $form_state, $entity, $delta, $region, $plugin_id, $configuration); + } + + /** + * {@inheritdoc} + */ + protected function submitLabel() { + return $this->t('Update'); + } + +} diff --git a/core/modules/layout_builder/src/LayoutSectionBuilder.php b/core/modules/layout_builder/src/LayoutSectionBuilder.php index fdd6b365fc..1682974f11 100644 --- a/core/modules/layout_builder/src/LayoutSectionBuilder.php +++ b/core/modules/layout_builder/src/LayoutSectionBuilder.php @@ -15,6 +15,8 @@ /** * Builds the UI for layout sections. + * + * @internal */ class LayoutSectionBuilder { diff --git a/core/modules/layout_builder/src/LayoutTempstoreRepository.php b/core/modules/layout_builder/src/LayoutTempstoreRepository.php index 652e9c76d9..87baa1331c 100644 --- a/core/modules/layout_builder/src/LayoutTempstoreRepository.php +++ b/core/modules/layout_builder/src/LayoutTempstoreRepository.php @@ -9,6 +9,8 @@ /** * Provides a mechanism for loading layouts from tempstore. + * + * @internal */ class LayoutTempstoreRepository implements LayoutTempstoreRepositoryInterface { @@ -43,13 +45,14 @@ public function __construct(SharedTempStoreFactory $temp_store_factory, EntityTy * {@inheritdoc} */ public function get(EntityInterface $entity) { - list($collection, $id) = $this->generateTempstoreId($entity); - $tempstore = $this->tempStoreFactory->get($collection)->get($id); + $id = $this->generateTempstoreId($entity); + $tempstore = $this->getTempstore($entity)->get($id); if (!empty($tempstore['entity'])) { + $entity_type_id = $entity->getEntityTypeId(); $entity = $tempstore['entity']; if (!($entity instanceof EntityInterface)) { - throw new \UnexpectedValueException(sprintf('The entry for collection "%s" and ID "%s" is not a valid entity', $collection, $id)); + throw new \UnexpectedValueException(sprintf('The entry with entity type "%s" and ID "%s" is not a valid entity', $entity_type_id, $id)); } } return $entity; @@ -67,8 +70,8 @@ public function getFromId($entity_type_id, $entity_id) { * {@inheritdoc} */ public function set(EntityInterface $entity) { - list($collection, $id) = $this->generateTempstoreId($entity); - $this->tempStoreFactory->get($collection)->set($id, ['entity' => $entity]); + $id = $this->generateTempstoreId($entity); + $this->getTempstore($entity)->set($id, ['entity' => $entity]); } /** @@ -76,27 +79,40 @@ public function set(EntityInterface $entity) { */ public function delete(EntityInterface $entity) { if ($this->get($entity)) { - list($collection, $id) = $this->generateTempstoreId($entity); - $this->tempStoreFactory->get($collection)->delete($id); + $id = $this->generateTempstoreId($entity); + $this->getTempstore($entity)->delete($id); } } /** - * Generates a collection and ID for putting an entity in tempstore. + * Generates an ID for putting an entity in tempstore. * * @param \Drupal\Core\Entity\EntityInterface $entity * The entity being stored. * - * @return array - * An array containing the collection name and the tempstore ID. + * @return string + * The tempstore ID. */ protected function generateTempstoreId(EntityInterface $entity) { - $collection = $entity->getEntityTypeId() . '.layout_builder__layout'; $id = "{$entity->id()}.{$entity->language()->getId()}"; if ($entity instanceof RevisionableInterface) { $id .= '.' . $entity->getRevisionId(); } - return [$collection, $id]; + return $id; + } + + /** + * Gets the shared tempstore. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity being stored. + * + * @return \Drupal\user\SharedTempStore + * The tempstore. + */ + protected function getTempstore(EntityInterface $entity) { + $collection = $entity->getEntityTypeId() . '.layout_builder__layout'; + return $this->tempStoreFactory->get($collection); } } diff --git a/core/modules/layout_builder/src/LayoutTempstoreRepositoryInterface.php b/core/modules/layout_builder/src/LayoutTempstoreRepositoryInterface.php index af6f382d35..ffce1c3008 100644 --- a/core/modules/layout_builder/src/LayoutTempstoreRepositoryInterface.php +++ b/core/modules/layout_builder/src/LayoutTempstoreRepositoryInterface.php @@ -6,6 +6,11 @@ /** * Provides an interface for loading layouts from tempstore. + * + * @internal + * Layout Builder is currently experimental and should only be leveraged by + * experimental modules and development releases of contributed modules. + * See https://www.drupal.org/core/experimental for more information. */ interface LayoutTempstoreRepositoryInterface { diff --git a/core/modules/layout_builder/src/Plugin/Derivative/LayoutBuilderLocalTaskDeriver.php b/core/modules/layout_builder/src/Plugin/Derivative/LayoutBuilderLocalTaskDeriver.php index 7b3a1762a1..02a1ea14df 100644 --- a/core/modules/layout_builder/src/Plugin/Derivative/LayoutBuilderLocalTaskDeriver.php +++ b/core/modules/layout_builder/src/Plugin/Derivative/LayoutBuilderLocalTaskDeriver.php @@ -5,7 +5,6 @@ use Drupal\Component\Plugin\Derivative\DeriverBase; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; -use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\layout_builder\Plugin\Menu\LayoutBuilderLocalTask; @@ -51,8 +50,8 @@ public static function create(ContainerInterface $container, $base_plugin_id) { */ public function getDerivativeDefinitions($base_plugin_definition) { foreach (array_keys($this->getEntityTypes()) as $entity_type_id) { - $this->derivatives["entity.$entity_type_id.layout"] = $base_plugin_definition + [ - 'route_name' => "entity.$entity_type_id.layout", + $this->derivatives["entity.$entity_type_id.layout_builder"] = $base_plugin_definition + [ + 'route_name' => "entity.$entity_type_id.layout_builder", 'weight' => 15, 'title' => $this->t('Layout'), 'base_route' => "entity.$entity_type_id.canonical", @@ -62,14 +61,14 @@ public function getDerivativeDefinitions($base_plugin_definition) { $this->derivatives["entity.$entity_type_id.save_layout"] = $base_plugin_definition + [ 'route_name' => "entity.$entity_type_id.save_layout", 'title' => $this->t('Save Layout'), - 'parent_id' => "layout_builder_ui:entity.$entity_type_id.layout", + 'parent_id' => "layout_builder_ui:entity.$entity_type_id.layout_builder", 'entity_type_id' => $entity_type_id, 'class' => LayoutBuilderLocalTask::class, ]; $this->derivatives["entity.$entity_type_id.cancel_layout"] = $base_plugin_definition + [ 'route_name' => "entity.$entity_type_id.cancel_layout", 'title' => $this->t('Cancel Layout'), - 'parent_id' => "layout_builder_ui:entity.$entity_type_id.layout", + 'parent_id' => "layout_builder_ui:entity.$entity_type_id.layout_builder", 'entity_type_id' => $entity_type_id, 'class' => LayoutBuilderLocalTask::class, 'weight' => 5, @@ -87,7 +86,7 @@ public function getDerivativeDefinitions($base_plugin_definition) { */ protected function getEntityTypes() { return array_filter($this->entityTypeManager->getDefinitions(), function (EntityTypeInterface $entity_type) { - return $entity_type->entityClassImplements(FieldableEntityInterface::class) && $entity_type->hasLinkTemplate('canonical') && $entity_type->hasViewBuilderClass(); + return $entity_type->hasLinkTemplate('layout-builder'); }); } diff --git a/core/modules/layout_builder/src/Plugin/Field/FieldFormatter/LayoutSectionFormatter.php b/core/modules/layout_builder/src/Plugin/Field/FieldFormatter/LayoutSectionFormatter.php index bbc8b2f3c6..4951d01c4b 100644 --- a/core/modules/layout_builder/src/Plugin/Field/FieldFormatter/LayoutSectionFormatter.php +++ b/core/modules/layout_builder/src/Plugin/Field/FieldFormatter/LayoutSectionFormatter.php @@ -12,6 +12,8 @@ /** * Plugin implementation of the 'layout_section' formatter. * + * @internal + * * @FieldFormatter( * id = "layout_section", * label = @Translation("Layout Section"), @@ -76,7 +78,7 @@ public static function create(ContainerInterface $container, array $configuratio public function viewElements(FieldItemListInterface $items, $langcode) { $elements = []; - /** @var \Drupal\layout_builder\LayoutSectionItemInterface[] $items */ + /** @var \Drupal\layout_builder\Field\LayoutSectionItemInterface[] $items */ foreach ($items as $delta => $item) { $elements[$delta] = $this->builder->buildSection($item->layout, $item->layout_settings, $item->section); } diff --git a/core/modules/layout_builder/src/Plugin/Field/FieldType/LayoutSectionItem.php b/core/modules/layout_builder/src/Plugin/Field/FieldType/LayoutSectionItem.php index 136073b11f..490aee48d3 100644 --- a/core/modules/layout_builder/src/Plugin/Field/FieldType/LayoutSectionItem.php +++ b/core/modules/layout_builder/src/Plugin/Field/FieldType/LayoutSectionItem.php @@ -8,11 +8,13 @@ use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\Core\TypedData\DataDefinition; use Drupal\Core\TypedData\MapDataDefinition; -use Drupal\layout_builder\LayoutSectionItemInterface; +use Drupal\layout_builder\Field\LayoutSectionItemInterface; /** * Plugin implementation of the 'layout_section' field type. * + * @internal + * * @FieldType( * id = "layout_section", * label = @Translation("Layout Section"), diff --git a/core/modules/layout_builder/src/Routing/LayoutBuilderRoutes.php b/core/modules/layout_builder/src/Routing/LayoutBuilderRoutes.php index d4dfecbf04..896384f5cd 100644 --- a/core/modules/layout_builder/src/Routing/LayoutBuilderRoutes.php +++ b/core/modules/layout_builder/src/Routing/LayoutBuilderRoutes.php @@ -2,6 +2,7 @@ namespace Drupal\layout_builder\Routing; +use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Entity\FieldableEntityInterface; @@ -21,14 +22,24 @@ class LayoutBuilderRoutes { */ protected $entityTypeManager; + /** + * The entity field manager. + * + * @var \Drupal\Core\Entity\EntityFieldManagerInterface + */ + protected $entityFieldManager; + /** * Constructs a new LayoutBuilderRoutes. * * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * The entity type manager. + * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager + * The entity field manager. */ - public function __construct(EntityTypeManagerInterface $entity_type_manager) { + public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager) { $this->entityTypeManager = $entity_type_manager; + $this->entityFieldManager = $entity_field_manager; } /** @@ -41,8 +52,10 @@ public function getRoutes() { $routes = []; foreach ($this->getEntityTypes() as $entity_type_id => $entity_type) { - $template = $entity_type->getLinkTemplate('canonical'); - $route = (new Route("$template/layout")) + $integer_id = $this->hasIntegerId($entity_type); + + $template = $entity_type->getLinkTemplate('layout-builder'); + $route = (new Route($template)) ->setDefaults([ '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::layout', '_title_callback' => '\Drupal\layout_builder\Controller\LayoutBuilderController::title', @@ -50,7 +63,6 @@ public function getRoutes() { 'entity_type_id' => $entity_type_id, ]) ->addRequirements([ - $entity_type_id => '\d+', '_has_layout_section' => 'true', ]) ->addOptions([ @@ -62,16 +74,18 @@ public function getRoutes() { ], ], ]); - $routes["entity.$entity_type_id.layout"] = $route; + if ($integer_id) { + $route->setRequirement($entity_type_id, '\d+'); + } + $routes["entity.$entity_type_id.layout_builder"] = $route; - $route = (new Route("$template/layout/save")) + $route = (new Route("$template/save")) ->setDefaults([ '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::saveLayout', 'entity' => NULL, 'entity_type_id' => $entity_type_id, ]) ->addRequirements([ - $entity_type_id => '\d+', '_has_layout_section' => 'true', ]) ->addOptions([ @@ -83,16 +97,18 @@ public function getRoutes() { ], ], ]); + if ($integer_id) { + $route->setRequirement($entity_type_id, '\d+'); + } $routes["entity.$entity_type_id.save_layout"] = $route; - $route = (new Route("$template/layout/cancel")) + $route = (new Route("$template/cancel")) ->setDefaults([ '_controller' => '\Drupal\layout_builder\Controller\LayoutBuilderController::cancelLayout', 'entity' => NULL, 'entity_type_id' => $entity_type_id, ]) ->addRequirements([ - $entity_type_id => '\d+', '_has_layout_section' => 'true', ]) ->addOptions([ @@ -104,11 +120,28 @@ public function getRoutes() { ], ], ]); + if ($integer_id) { + $route->setRequirement($entity_type_id, '\d+'); + } $routes["entity.$entity_type_id.cancel_layout"] = $route; } return $routes; } + /** + * Determines if this entity type's ID is stored as an integer. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * An entity type. + * + * @return bool + * TRUE if this entity type's ID key is always an integer, FALSE otherwise. + */ + protected function hasIntegerId(EntityTypeInterface $entity_type) { + $field_storage_definitions = $this->entityFieldManager->getFieldStorageDefinitions($entity_type->id()); + return $field_storage_definitions[$entity_type->getKey('id')]->getType() === 'integer'; + } + /** * Returns an array of relevant entity types. * @@ -117,7 +150,7 @@ public function getRoutes() { */ protected function getEntityTypes() { return array_filter($this->entityTypeManager->getDefinitions(), function (EntityTypeInterface $entity_type) { - return $entity_type->entityClassImplements(FieldableEntityInterface::class) && $entity_type->hasLinkTemplate('canonical') && $entity_type->hasViewBuilderClass(); + return $entity_type->hasLinkTemplate('layout-builder'); }); } diff --git a/core/modules/layout_builder/tests/src/Functional/LayoutSectionTest.php b/core/modules/layout_builder/tests/src/Functional/LayoutSectionTest.php index 0e30d14d04..61e481c7c7 100644 --- a/core/modules/layout_builder/tests/src/Functional/LayoutSectionTest.php +++ b/core/modules/layout_builder/tests/src/Functional/LayoutSectionTest.php @@ -162,12 +162,12 @@ public function providerTestLayoutSectionFormatter() { * @dataProvider providerTestLayoutSectionFormatter */ public function testLayoutSectionFormatter($layout_data, $expected_selector, $expected_content, $expected_cache_contexts, $expected_cache_tags, $expected_dynamic_cache) { - $this->createSectionNode($layout_data); + $node = $this->createSectionNode($layout_data); - $this->drupalGet('node/1'); + $this->drupalGet($node->toUrl('canonical')); $this->assertLayoutSection($expected_selector, $expected_content, $expected_cache_contexts, $expected_cache_tags, $expected_dynamic_cache); - $this->drupalGet('node/1/layout'); + $this->drupalGet($node->toUrl('layout-builder')); $this->assertLayoutSection($expected_selector, $expected_content, $expected_cache_contexts, $expected_cache_tags, 'UNCACHEABLE'); } @@ -175,7 +175,7 @@ public function testLayoutSectionFormatter($layout_data, $expected_selector, $ex * Tests the access checking of the section formatter. */ public function testLayoutSectionFormatterAccess() { - $this->createSectionNode([ + $node = $this->createSectionNode([ [ 'layout' => 'layout_onecol', 'section' => [ @@ -193,14 +193,14 @@ public function testLayoutSectionFormatterAccess() { // Restrict access to the block. $this->container->get('state')->set('test_block_access', FALSE); - $this->drupalGet('node/1'); + $this->drupalGet($node->toUrl('canonical')); $this->assertLayoutSection('.layout--onecol', NULL, '', '', 'UNCACHEABLE'); // Ensure the block was not rendered. $this->assertSession()->pageTextNotContains('Hello test world'); // Grant access to the block, and ensure it was rendered. $this->container->get('state')->set('test_block_access', TRUE); - $this->drupalGet('node/1'); + $this->drupalGet($node->toUrl('canonical')); $this->assertLayoutSection('.layout--onecol', 'Hello test world', '', '', 'UNCACHEABLE'); } @@ -256,9 +256,9 @@ public function testMultilingualLayoutSectionFormatter() { ]); $entity->save(); - $this->drupalGet('node/1'); + $this->drupalGet($entity->toUrl('canonical')); $this->assertLayoutSection('.layout--onecol', 'Powered by'); - $this->drupalGet('es/node/1'); + $this->drupalGet($entity->toUrl('canonical')->setOption('prefix', 'es/')); $this->assertLayoutSection('.layout--twocol', ['foo text', 'bar text']); } @@ -267,9 +267,9 @@ public function testMultilingualLayoutSectionFormatter() { */ public function testLayoutPageTitle() { $this->drupalPlaceBlock('page_title_block'); - $this->createSectionNode([]); + $node = $this->createSectionNode([]); - $this->drupalGet('node/1/layout'); + $this->drupalGet($node->toUrl('layout-builder')); $this->assertSession()->titleEquals('Edit layout for The node title | Drupal'); $this->assertEquals('Edit layout for The node title', $this->cssSelect('h1.page-title')[0]->getText()); } @@ -278,7 +278,7 @@ public function testLayoutPageTitle() { * Tests that no Layout link shows without a section field. */ public function testLayoutUrlNoSectionField() { - $this->createNode([ + $node = $this->createNode([ 'type' => 'bundle_without_section_field', 'title' => 'The node title', 'body' => [ @@ -287,7 +287,8 @@ public function testLayoutUrlNoSectionField() { ], ], ]); - $this->drupalGet('node/1/layout'); + $node->save(); + $this->drupalGet($node->toUrl('layout-builder')); $this->assertSession()->statusCodeEquals(403); } diff --git a/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderTest.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderTest.php index ecfff5680e..1bf83bca47 100644 --- a/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderTest.php +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderTest.php @@ -4,6 +4,7 @@ use Drupal\block_content\Entity\BlockContent; use Drupal\block_content\Entity\BlockContentType; +use Drupal\Core\Url; use Drupal\FunctionalJavascriptTests\JavascriptTestBase; use Drupal\Tests\contextual\FunctionalJavascript\ContextualLinkClickTrait; @@ -28,6 +29,13 @@ class LayoutBuilderTest extends JavascriptTestBase { 'layout_test', ]; + /** + * The node to customize with Layout Builder. + * + * @var \Drupal\node\NodeInterface + */ + protected $node; + /** * {@inheritdoc} */ @@ -54,7 +62,7 @@ protected function setUp() { ])->save(); $this->createContentType(['type' => 'bundle_with_section_field']); - $this->createNode([ + $this->node = $this->createNode([ 'type' => 'bundle_with_section_field', 'title' => 'The node title', 'body' => [ @@ -80,12 +88,12 @@ protected function setUp() { /** * Tests the Layout Builder UI. */ - public function test() { + public function testLayoutBuilderUi() { $assert_session = $this->assertSession(); $page = $this->getSession()->getPage(); // Ensure the block is not displayed initially. - $this->drupalGet('node/1'); + $this->drupalGet($this->node->toUrl('canonical')); $assert_session->pageTextNotContains('Powered by Drupal'); // Enter the layout editing mode. @@ -124,28 +132,28 @@ public function test() { $assert_session->assertWaitOnAjaxRequest(); $assert_session->elementNotExists('css', '#drupal-off-canvas'); - $assert_session->addressEquals('node/1/layout'); + $assert_session->addressEquals($this->node->toUrl('layout-builder')); $assert_session->pageTextContains('Powered by Drupal'); $assert_session->pageTextContains('This is the label'); $this->assertPageNotReloaded(); // Until the layout is saved, the new block is not visible on the node page. - $this->drupalGet('node/1'); + $this->drupalGet($this->node->toUrl('canonical')); $assert_session->pageTextNotContains('Powered by Drupal'); // When returning to the layout edit mode, the new block is visible. - $this->drupalGet('node/1/layout'); + $this->drupalGet($this->node->toUrl('layout-builder')); $assert_session->pageTextContains('Powered by Drupal'); // Save the layout, and the new block is visible. $this->clickLink('Save Layout'); - $assert_session->addressEquals('node/1'); + $assert_session->addressEquals($this->node->toUrl('canonical')); $assert_session->pageTextContains('Powered by Drupal'); $assert_session->pageTextContains('This is the label'); $assert_session->elementExists('css', '.layout'); // Drag one block from one region to another. - $this->drupalGet('node/1/layout'); + $this->drupalGet($this->node->toUrl('layout-builder')); $this->markCurrentPage(); $this->clickLink('Add Section'); @@ -165,7 +173,7 @@ public function test() { $this->assertPageNotReloaded(); // Ensure the drag persisted after reload. - $this->drupalGet('node/1/layout'); + $this->drupalGet($this->node->toUrl('layout-builder')); $assert_session->elementExists('css', '.layout__region--second .block-system-powered-by-block'); $assert_session->elementTextContains('css', '.layout__region--second', 'Powered by Drupal'); @@ -175,7 +183,7 @@ public function test() { $assert_session->elementTextContains('css', '.layout__region--second', 'Powered by Drupal'); // Configure a block. - $this->drupalGet('node/1/layout'); + $this->drupalGet($this->node->toUrl('layout-builder')); $this->markCurrentPage(); $this->clickContextualLink('.block-system-powered-by-block', 'Configure'); @@ -187,7 +195,7 @@ public function test() { $assert_session->assertWaitOnAjaxRequest(); $assert_session->elementNotExists('css', '#drupal-off-canvas'); - $assert_session->addressEquals('node/1/layout'); + $assert_session->addressEquals($this->node->toUrl('layout-builder')); $assert_session->pageTextContains('Powered by Drupal'); $assert_session->pageTextContains('This is the new label'); $assert_session->pageTextNotContains('This is the label'); @@ -203,14 +211,14 @@ public function test() { $assert_session->pageTextNotContains('Powered by Drupal'); $assert_session->linkExists('Add Block'); - $assert_session->addressEquals('node/1/layout'); + $assert_session->addressEquals($this->node->toUrl('layout-builder')); $this->assertPageNotReloaded(); $this->clickLink('Save Layout'); $assert_session->elementExists('css', '.layout'); // Test deriver-based blocks. - $this->drupalGet('node/1/layout'); + $this->drupalGet($this->node->toUrl('layout-builder')); $this->markCurrentPage(); $this->clickLink('Add Block'); @@ -251,7 +259,7 @@ public function testConfigurableLayouts() { $assert_session = $this->assertSession(); $page = $this->getSession()->getPage(); - $this->drupalGet('node/1/layout'); + $this->drupalGet($this->node->toUrl('layout-builder')); $this->markCurrentPage(); $this->clickLink('Add Section'); @@ -295,21 +303,27 @@ public function testLayoutNoDialog() { $page = $this->getSession()->getPage(); // Set up a layout with one section. - $this->drupalGet('node/1/layout'); + $this->drupalGet($this->node->toUrl('layout-builder')); $this->clickLink('Add Section'); $assert_session->assertWaitOnAjaxRequest(); $this->clickLink('One column'); $assert_session->assertWaitOnAjaxRequest(); // Test bypassing the Off Canvas dialog by visiting the URL directly. - $this->drupalGet('layout_builder/add/block/node/1/0/content/system_powered_by_block'); + $this->drupalGet(Url::fromRoute('layout_builder.add_block', [ + 'entity_type_id' => 'node', + 'entity' => 1, + 'delta' => 0, + 'region' => 'content', + 'plugin_id' => 'system_powered_by_block', + ])); $assert_session->elementNotExists('css', '#drupal-off-canvas'); $page->fillField('settings[label]', 'The block label'); $page->fillField('settings[label_display]', TRUE); $page->pressButton('Add Block'); $assert_session->assertWaitOnAjaxRequest(); - $assert_session->addressEquals('node/1/layout'); + $assert_session->addressEquals($this->node->toUrl('layout-builder')); $assert_session->pageTextContains('Powered by Drupal'); $assert_session->pageTextContains('The block label'); } diff --git a/core/modules/layout_builder/tests/src/Kernel/LayoutSectionItemTest.php b/core/modules/layout_builder/tests/src/Kernel/LayoutSectionItemTest.php index 76d7ea74c0..0e13471c15 100644 --- a/core/modules/layout_builder/tests/src/Kernel/LayoutSectionItemTest.php +++ b/core/modules/layout_builder/tests/src/Kernel/LayoutSectionItemTest.php @@ -5,7 +5,7 @@ use Drupal\Core\Field\FieldItemInterface; use Drupal\Core\Field\FieldItemListInterface; use Drupal\entity_test\Entity\EntityTest; -use Drupal\layout_builder\LayoutSectionItemInterface; +use Drupal\layout_builder\Field\LayoutSectionItemInterface; use Drupal\layout_builder\Field\LayoutSectionItemListInterface; use Drupal\Tests\field\Kernel\FieldKernelTestBase; diff --git a/core/modules/layout_builder/tests/src/Unit/LayoutTempstoreRepositoryTest.php b/core/modules/layout_builder/tests/src/Unit/LayoutTempstoreRepositoryTest.php index faef394d4f..a652d4a56d 100644 --- a/core/modules/layout_builder/tests/src/Unit/LayoutTempstoreRepositoryTest.php +++ b/core/modules/layout_builder/tests/src/Unit/LayoutTempstoreRepositoryTest.php @@ -126,7 +126,7 @@ public function testGetInvalidEntity() { $entity->getEntityTypeId()->willReturn('the_entity_type_id'); $entity->id()->willReturn('the_entity_id'); - $this->setExpectedException(\UnexpectedValueException::class, 'The entry for collection "the_entity_type_id.layout_builder__layout" and ID "the_entity_id.en" is not a valid entity'); + $this->setExpectedException(\UnexpectedValueException::class, 'The entry with entity type "the_entity_type_id" and ID "the_entity_id.en" is not a valid entity'); $repository->get($entity->reveal()); }