From b733ce6c65ac723bd3146ef40bfa13a7ef1a16d6 Mon Sep 17 00:00:00 2001 From: Florent Torregrosa Date: Wed, 17 Jul 2024 11:11:44 +0200 Subject: [PATCH] Issue #3134371 by penyaskito, dsnopek, srishtiiee: Allow changing the layout of an existing section in Layout Builder UI --- .../layout_builder/css/layout-builder.css | 4 + .../css/layout-builder.pcss.css | 5 + .../layout_builder/js/layout-builder.js | 1 - .../layout_builder/layout_builder.routing.yml | 26 ++ .../ChangeSectionLayoutController.php | 107 ++++++++ .../src/Element/LayoutBuilder.php | 100 ++++--- .../Form/ConfigureNewSectionLayoutForm.php | 120 +++++++++ .../src/Form/ConfigureSectionForm.php | 242 +---------------- .../src/Form/SectionFormBase.php | 254 ++++++++++++++++++ .../src/Functional/LayoutBuilderTest.php | 174 +++++++++++- .../LayoutBuilderTest.php | 25 ++ 11 files changed, 784 insertions(+), 274 deletions(-) create mode 100644 core/modules/layout_builder/src/Controller/ChangeSectionLayoutController.php create mode 100644 core/modules/layout_builder/src/Form/ConfigureNewSectionLayoutForm.php create mode 100644 core/modules/layout_builder/src/Form/SectionFormBase.php diff --git a/core/modules/layout_builder/css/layout-builder.css b/core/modules/layout_builder/css/layout-builder.css index d4c48248de6a..88266d4fb86d 100644 --- a/core/modules/layout_builder/css/layout-builder.css +++ b/core/modules/layout_builder/css/layout-builder.css @@ -58,6 +58,10 @@ background-color: #ffd; } +.layout-builder__section .layout-builder__section_links a.layout-builder__link:not(:first-child, :last-child)::after { + content: " | "; +} + .layout-builder__region { outline: 2px dashed #2f91da; } diff --git a/core/modules/layout_builder/css/layout-builder.pcss.css b/core/modules/layout_builder/css/layout-builder.pcss.css index 7ddb9543e007..373144b86cd6 100644 --- a/core/modules/layout_builder/css/layout-builder.pcss.css +++ b/core/modules/layout_builder/css/layout-builder.pcss.css @@ -49,6 +49,11 @@ outline: 2px dashed #fedb60; background-color: #ffd; } + + & .layout-builder__section_links a.layout-builder__link:not(:first-child,:last-child)::after { + content: " | "; + } + } .layout-builder__region { diff --git a/core/modules/layout_builder/js/layout-builder.js b/core/modules/layout_builder/js/layout-builder.js index bf5b24ac2a93..9b43afe875ea 100644 --- a/core/modules/layout_builder/js/layout-builder.js +++ b/core/modules/layout_builder/js/layout-builder.js @@ -458,7 +458,6 @@ return `
${contentPreviewPlaceholderText}
`; }; - // Remove all contextual links outside the layout. $(window).on('drupalContextualLinkAdded', (event, data) => { const element = data.$el; diff --git a/core/modules/layout_builder/layout_builder.routing.yml b/core/modules/layout_builder/layout_builder.routing.yml index fa72dcec931f..afa2b247ab00 100644 --- a/core/modules/layout_builder/layout_builder.routing.yml +++ b/core/modules/layout_builder/layout_builder.routing.yml @@ -39,6 +39,32 @@ layout_builder.configure_section: section_storage: layout_builder_tempstore: TRUE +layout_builder.change_section_layout: + path: '/layout_builder/change_layout/section/{section_storage_type}/{section_storage}/{delta}' + defaults: + _controller: '\Drupal\layout_builder\Controller\ChangeSectionLayoutController::build' + _title: 'Change layout for this section' + requirements: + _layout_builder_access: 'view' + options: + _admin_route: TRUE + parameters: + section_storage: + layout_builder_tempstore: TRUE + +layout_builder.configure_changed_section_layout: + path: '/layout_builder/map_regions/section/{section_storage_type}/{section_storage}/{delta}/{plugin_id}' + defaults: + _title: 'Configure new layout' + _form: '\Drupal\layout_builder\Form\ConfigureNewSectionLayoutForm' + requirements: + _layout_builder_access: 'view' + options: + _admin_route: TRUE + parameters: + section_storage: + layout_builder_tempstore: TRUE + layout_builder.remove_section: path: '/layout_builder/remove/section/{section_storage_type}/{section_storage}/{delta}' defaults: diff --git a/core/modules/layout_builder/src/Controller/ChangeSectionLayoutController.php b/core/modules/layout_builder/src/Controller/ChangeSectionLayoutController.php new file mode 100644 index 000000000000..c006e4942b09 --- /dev/null +++ b/core/modules/layout_builder/src/Controller/ChangeSectionLayoutController.php @@ -0,0 +1,107 @@ +layoutManager = $layout_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('plugin.manager.core.layout') + ); + } + + /** + * Choose a layout plugin to add as a section. + * + * @param \Drupal\layout_builder\SectionStorageInterface $section_storage + * The section storage. + * @param int $delta + * The delta of the section to splice. + * + * @return array + * The render array. + */ + public function build(SectionStorageInterface $section_storage, int $delta) { + $items = []; + $definitions = $this->layoutManager->getFilteredDefinitions('layout_builder', [], ['section_storage' => $section_storage]); + foreach ($definitions as $plugin_id => $definition) { + $item = [ + '#type' => 'link', + '#title' => [ + 'icon' => $definition->getIcon(60, 80, 1, 3), + 'label' => [ + '#type' => 'container', + '#children' => $definition->getLabel(), + ], + ], + '#url' => Url::fromRoute( + 'layout_builder.configure_changed_section_layout', + [ + 'section_storage_type' => $section_storage->getStorageType(), + 'section_storage' => $section_storage->getStorageId(), + 'delta' => $delta, + 'plugin_id' => $plugin_id, + ] + ), + ]; + if ($this->isAjax()) { + $item['#attributes']['class'][] = 'use-ajax'; + $item['#attributes']['data-dialog-type'][] = 'dialog'; + $item['#attributes']['data-dialog-renderer'][] = 'off_canvas'; + } + $items[$plugin_id] = $item; + } + $output['layouts'] = [ + '#theme' => 'item_list__layouts', + '#items' => $items, + '#attributes' => [ + 'class' => [ + 'layout-selection', + ], + 'data-layout-builder-target-highlight-id' => $this->sectionAddHighlightId($delta), + ], + ]; + + return $output; + } + +} diff --git a/core/modules/layout_builder/src/Element/LayoutBuilder.php b/core/modules/layout_builder/src/Element/LayoutBuilder.php index 2676d5a0dba6..258c7786449e 100644 --- a/core/modules/layout_builder/src/Element/LayoutBuilder.php +++ b/core/modules/layout_builder/src/Element/LayoutBuilder.php @@ -332,47 +332,73 @@ protected function buildAdministrativeSection(SectionStorageInterface $section_s 'role' => 'group', 'aria-label' => $section_label, ], - 'remove' => [ - '#type' => 'link', - '#title' => $this->t('Remove @section', ['@section' => $section_label]), - '#url' => Url::fromRoute('layout_builder.remove_section', [ - 'section_storage_type' => $storage_type, - 'section_storage' => $storage_id, - 'delta' => $delta, - ]), + 'section_links' => [ + '#type' => 'container', '#attributes' => [ - 'class' => [ - 'use-ajax', - 'layout-builder__link', - 'layout-builder__link--remove', + 'class' => ['layout-builder__section_links'], + 'role' => 'group', + 'aria-label' => $this->t('Administration links for @section', ['@section' => $section_label]), + ], + 'remove' => [ + '#type' => 'link', + '#title' => $this->t('Remove @section', ['@section' => $section_label]), + '#url' => Url::fromRoute('layout_builder.remove_section', [ + 'section_storage_type' => $storage_type, + 'section_storage' => $storage_id, + 'delta' => $delta, + ]), + '#attributes' => [ + 'class' => [ + 'use-ajax', + 'layout-builder__link', + 'layout-builder__link--remove', + ], + 'data-dialog-type' => 'dialog', + 'data-dialog-renderer' => 'off_canvas', ], - 'data-dialog-type' => 'dialog', - 'data-dialog-renderer' => 'off_canvas', ], - ], - // The section label is added to sections without a "Configure section" - // link, and is only visible when the move block dialog is open. - 'section_label' => [ - '#markup' => $this->t('', ['@section' => $section_label]), - '#access' => !$layout instanceof PluginFormInterface, - ], - 'configure' => [ - '#type' => 'link', - '#title' => $this->t('Configure @section', ['@section' => $section_label]), - '#access' => $layout instanceof PluginFormInterface, - '#url' => Url::fromRoute('layout_builder.configure_section', [ - 'section_storage_type' => $storage_type, - 'section_storage' => $storage_id, - 'delta' => $delta, - ]), - '#attributes' => [ - 'class' => [ - 'use-ajax', - 'layout-builder__link', - 'layout-builder__link--configure', + // The section label is added to sections without a "Configure section" + // link, and is only visible when the move block dialog is open. + 'section_label' => [ + '#markup' => $this->t('', ['@section' => $section_label]), + '#access' => !$layout instanceof PluginFormInterface, + ], + 'configure' => [ + '#type' => 'link', + '#title' => $this->t('Configure @section', ['@section' => $section_label]), + '#access' => $layout instanceof PluginFormInterface, + '#url' => Url::fromRoute('layout_builder.configure_section', [ + 'section_storage_type' => $storage_type, + 'section_storage' => $storage_id, + 'delta' => $delta, + ]), + '#attributes' => [ + 'class' => [ + 'use-ajax', + 'layout-builder__link', + 'layout-builder__link--configure', + ], + 'data-dialog-type' => 'dialog', + 'data-dialog-renderer' => 'off_canvas', + ], + ], + 'change_layout' => [ + '#type' => 'link', + '#title' => $this->t('Change layout for @section', ['@section' => $section_label]), + '#url' => Url::fromRoute('layout_builder.change_section_layout', [ + 'section_storage_type' => $storage_type, + 'section_storage' => $storage_id, + 'delta' => $delta, + ]), + '#attributes' => [ + 'class' => [ + 'use-ajax', + 'layout-builder__link', + 'layout-builder__link--change-layout', + ], + 'data-dialog-type' => 'dialog', + 'data-dialog-renderer' => 'off_canvas', ], - 'data-dialog-type' => 'dialog', - 'data-dialog-renderer' => 'off_canvas', ], ], 'layout-builder__section' => $build, diff --git a/core/modules/layout_builder/src/Form/ConfigureNewSectionLayoutForm.php b/core/modules/layout_builder/src/Form/ConfigureNewSectionLayoutForm.php new file mode 100644 index 000000000000..7658f0e6e855 --- /dev/null +++ b/core/modules/layout_builder/src/Form/ConfigureNewSectionLayoutForm.php @@ -0,0 +1,120 @@ +layoutPluginManager = $layout_plugin_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('layout_builder.tempstore_repository'), + $container->get('plugin_form.factory'), + $container->get('plugin.manager.core.layout') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'layout_builder_configure_new_section_layout'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state, SectionStorageInterface $section_storage = NULL, $delta = NULL, $plugin_id = NULL) { + $this->sectionStorage = $section_storage; + $this->delta = $delta; + + $this->layout = $this->layoutPluginManager->createInstance($plugin_id); + $form = parent::buildForm($form, $form_state); + + $section = $this->sectionStorage->getSection($this->delta); + $old_layout_region_labels = $section->getLayout()->getPluginDefinition()->getRegionLabels(); + $new_layout_region_labels = $this->layout->getPluginDefinition()->getRegionLabels(); + $new_layout_first_region = array_key_first($new_layout_region_labels); + $new_region_mapping = []; + foreach ($old_layout_region_labels as $region => $region_label) { + $new_region_mapping[$region] = isset($new_layout_region_labels[$region]) ? $region : $new_layout_first_region; + } + $form_state->set('new_region_mapping', $new_region_mapping); + + $form['actions']['submit']['#value'] = $this->t('Update'); + + $target_highlight_id = $this->sectionUpdateHighlightId($delta); + $form['#attributes']['data-layout-builder-target-highlight-id'] = $target_highlight_id; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + // Call the plugin submit handler. + $subform_state = SubformState::createForSubform($form['layout_settings'], $form, $form_state); + $this->getPluginForm($this->layout)->submitConfigurationForm($form['layout_settings'], $subform_state); + + $old_section = $this->sectionStorage->getSection($this->delta); + $third_party_settings = []; + foreach ($old_section->getThirdPartyProviders() as $provider) { + $third_party_settings[$provider] = $old_section->getThirdPartySettings($provider); + } + + $plugin_id = $this->layout->getPluginId(); + $configuration = $this->layout->getConfiguration(); + $new_section = new Section($plugin_id, $configuration, $old_section->getComponents(), $third_party_settings); + + $region_mapping = $form_state->get('new_region_mapping'); + foreach ($new_section->getComponents() as $component) { + $component->setRegion($region_mapping[$component->getRegion()]); + } + + $this->sectionStorage->removeSection($this->delta); + $this->sectionStorage->insertSection($this->delta, $new_section); + + $this->layoutTempstoreRepository->set($this->sectionStorage); + $form_state->setRedirectUrl($this->sectionStorage->getLayoutBuilderUrl()); + } + +} diff --git a/core/modules/layout_builder/src/Form/ConfigureSectionForm.php b/core/modules/layout_builder/src/Form/ConfigureSectionForm.php index 38e57cdb10a6..d88215d9da8b 100644 --- a/core/modules/layout_builder/src/Form/ConfigureSectionForm.php +++ b/core/modules/layout_builder/src/Form/ConfigureSectionForm.php @@ -2,24 +2,10 @@ namespace Drupal\layout_builder\Form; -use Drupal\Component\Utility\Html; -use Drupal\Core\Ajax\AjaxFormHelperTrait; -use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Form\SubformState; use Drupal\Core\Form\WorkspaceDynamicSafeFormInterface; -use Drupal\Core\Layout\LayoutInterface; -use Drupal\Core\Plugin\ContextAwarePluginInterface; -use Drupal\Core\Plugin\PluginFormFactoryInterface; -use Drupal\Core\Plugin\PluginFormInterface; -use Drupal\Core\Plugin\PluginWithFormsInterface; use Drupal\layout_builder\Context\LayoutBuilderContextTrait; -use Drupal\layout_builder\Controller\LayoutRebuildTrait; -use Drupal\layout_builder\LayoutBuilderHighlightTrait; -use Drupal\layout_builder\LayoutTempstoreRepositoryInterface; -use Drupal\layout_builder\Section; use Drupal\layout_builder\SectionStorageInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; /** * Provides a form for configuring a layout section. @@ -27,100 +13,11 @@ * @internal * Form classes are internal. */ -class ConfigureSectionForm extends FormBase implements WorkspaceDynamicSafeFormInterface { +class ConfigureSectionForm extends SectionFormBase implements WorkspaceDynamicSafeFormInterface { - use AjaxFormHelperTrait; use LayoutBuilderContextTrait; - use LayoutBuilderHighlightTrait; - use LayoutRebuildTrait; use WorkspaceSafeFormTrait; - /** - * The layout tempstore repository. - * - * @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface - */ - protected $layoutTempstoreRepository; - - /** - * The plugin being configured. - * - * @var \Drupal\Core\Layout\LayoutInterface|\Drupal\Core\Plugin\PluginFormInterface - */ - protected $layout; - - /** - * The section being configured. - * - * @var \Drupal\layout_builder\Section - */ - protected $section; - - /** - * The plugin form manager. - * - * @var \Drupal\Core\Plugin\PluginFormFactoryInterface - */ - protected $pluginFormFactory; - - /** - * The section storage. - * - * @var \Drupal\layout_builder\SectionStorageInterface - */ - protected $sectionStorage; - - /** - * The field delta. - * - * @var int - */ - protected $delta; - - /** - * The plugin ID. - * - * @var string - */ - protected $pluginId; - - /** - * Indicates whether the section is being added or updated. - * - * @var bool - */ - protected $isUpdate; - - /** - * Constructs a new ConfigureSectionForm. - * - * @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository - * The layout tempstore repository. - * @param \Drupal\Core\Plugin\PluginFormFactoryInterface $plugin_form_manager - * The plugin form manager. - */ - public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository, PluginFormFactoryInterface $plugin_form_manager) { - $this->layoutTempstoreRepository = $layout_tempstore_repository; - $this->pluginFormFactory = $plugin_form_manager; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container) { - return new static( - $container->get('layout_builder.tempstore_repository'), - $container->get('plugin_form.factory') - ); - } - - /** - * {@inheritdoc} - */ - public function getFormId() { - return 'layout_builder_configure_section'; - } - /** * {@inheritdoc} */ @@ -131,148 +28,23 @@ public function buildForm(array $form, FormStateInterface $form_state, ?SectionS $this->pluginId = $plugin_id; $section = $this->getCurrentSection(); - - if ($this->isUpdate) { - if ($label = $section->getLayoutSettings()['label']) { - $form['#title'] = $this->t('Configure @section', ['@section' => $label]); - } - } // Passing available contexts to the layout plugin here could result in an // exception since the layout may not have a context mapping for a required // context slot on creation. $this->layout = $section->getLayout(); - $form_state->setTemporaryValue('gathered_contexts', $this->getPopulatedContexts($this->sectionStorage)); - $form['#tree'] = TRUE; - $form['layout_settings'] = []; - $subform_state = SubformState::createForSubform($form['layout_settings'], $form, $form_state); - $form['layout_settings'] = $this->getPluginForm($this->layout)->buildConfigurationForm($form['layout_settings'], $subform_state); + $form = parent::buildForm($form, $form_state); - $form['actions']['submit'] = [ - '#type' => 'submit', - '#value' => $this->isUpdate ? $this->t('Update') : $this->t('Add section'), - '#button_type' => 'primary', - ]; - if ($this->isAjax()) { - $form['actions']['submit']['#ajax']['callback'] = '::ajaxSubmit'; - // @todo static::ajaxSubmit() requires data-drupal-selector to be the same - // between the various Ajax requests. A bug in - // \Drupal\Core\Form\FormBuilder prevents that from happening unless - // $form['#id'] is also the same. Normally, #id is set to a unique HTML - // ID via Html::getUniqueId(), but here we bypass that in order to work - // around the data-drupal-selector bug. This is okay so long as we - // assume that this form only ever occurs once on a page. Remove this - // workaround in https://www.drupal.org/node/2897377. - $form['#id'] = Html::getId($form_state->getBuildInfo()['form_id']); + if ($this->isUpdate) { + if ($label = $section->getLayoutSettings()['label']) { + $form['#title'] = $this->t('Configure @section', ['@section' => $label]); + } } + $target_highlight_id = $this->isUpdate ? $this->sectionUpdateHighlightId($delta) : $this->sectionAddHighlightId($delta); $form['#attributes']['data-layout-builder-target-highlight-id'] = $target_highlight_id; - // Mark this as an administrative page for JavaScript ("Back to site" link). - $form['#attached']['drupalSettings']['path']['currentPathIsAdmin'] = TRUE; return $form; } - /** - * {@inheritdoc} - */ - public function validateForm(array &$form, FormStateInterface $form_state) { - $subform_state = SubformState::createForSubform($form['layout_settings'], $form, $form_state); - $this->getPluginForm($this->layout)->validateConfigurationForm($form['layout_settings'], $subform_state); - } - - /** - * {@inheritdoc} - */ - public function submitForm(array &$form, FormStateInterface $form_state) { - // Call the plugin submit handler. - $subform_state = SubformState::createForSubform($form['layout_settings'], $form, $form_state); - $this->getPluginForm($this->layout)->submitConfigurationForm($form['layout_settings'], $subform_state); - - // If this layout is context-aware, set the context mapping. - if ($this->layout instanceof ContextAwarePluginInterface) { - $this->layout->setContextMapping($subform_state->getValue('context_mapping', [])); - } - - $configuration = $this->layout->getConfiguration(); - - $section = $this->getCurrentSection(); - $section->setLayoutSettings($configuration); - if (!$this->isUpdate) { - $this->sectionStorage->insertSection($this->delta, $section); - } - - $this->layoutTempstoreRepository->set($this->sectionStorage); - $form_state->setRedirectUrl($this->sectionStorage->getLayoutBuilderUrl()); - } - - /** - * {@inheritdoc} - */ - protected function successfulAjaxSubmit(array $form, FormStateInterface $form_state) { - return $this->rebuildAndClose($this->sectionStorage); - } - - /** - * Retrieves the plugin form for a given layout. - * - * @param \Drupal\Core\Layout\LayoutInterface $layout - * The layout plugin. - * - * @return \Drupal\Core\Plugin\PluginFormInterface - * The plugin form for the layout. - * - * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException - */ - protected function getPluginForm(LayoutInterface $layout) { - if ($layout instanceof PluginWithFormsInterface) { - return $this->pluginFormFactory->createInstance($layout, 'configure'); - } - - if ($layout instanceof PluginFormInterface) { - return $layout; - } - - throw new \InvalidArgumentException(sprintf('The "%s" layout does not provide a configuration form', $layout->getPluginId())); - } - - /** - * Retrieves the section storage property. - * - * @return \Drupal\layout_builder\SectionStorageInterface - * The section storage for the current form. - */ - public function getSectionStorage() { - return $this->sectionStorage; - } - - /** - * Retrieves the layout being modified by the form. - * - * @return \Drupal\Core\Layout\LayoutInterface|\Drupal\Core\Plugin\PluginFormInterface - * The layout for the current form. - */ - public function getCurrentLayout(): LayoutInterface { - return $this->layout; - } - - /** - * Retrieves the section being modified by the form. - * - * @return \Drupal\layout_builder\Section - * The section for the current form. - */ - public function getCurrentSection(): Section { - if (!isset($this->section)) { - if ($this->isUpdate) { - $this->section = $this->sectionStorage->getSection($this->delta); - } - else { - $this->section = new Section($this->pluginId); - } - } - - return $this->section; - } - } diff --git a/core/modules/layout_builder/src/Form/SectionFormBase.php b/core/modules/layout_builder/src/Form/SectionFormBase.php new file mode 100644 index 000000000000..ec997d27e746 --- /dev/null +++ b/core/modules/layout_builder/src/Form/SectionFormBase.php @@ -0,0 +1,254 @@ +layoutTempstoreRepository = $layout_tempstore_repository; + $this->pluginFormFactory = $plugin_form_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('layout_builder.tempstore_repository'), + $container->get('plugin_form.factory') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'layout_builder_configure_section'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state, SectionStorageInterface $section_storage = NULL, $delta = NULL, $plugin_id = NULL) { + $form['#tree'] = TRUE; + $form['layout_settings'] = []; + $subform_state = SubformState::createForSubform($form['layout_settings'], $form, $form_state); + $form['layout_settings'] = $this->getPluginForm($this->layout)->buildConfigurationForm($form['layout_settings'], $subform_state); + + $form['actions']['submit'] = [ + '#type' => 'submit', + '#value' => $this->isUpdate ? $this->t('Update') : $this->t('Add section'), + '#button_type' => 'primary', + ]; + if ($this->isAjax()) { + $form['actions']['submit']['#ajax']['callback'] = '::ajaxSubmit'; + // @todo static::ajaxSubmit() requires data-drupal-selector to be the same + // between the various Ajax requests. A bug in + // \Drupal\Core\Form\FormBuilder prevents that from happening unless + // $form['#id'] is also the same. Normally, #id is set to a unique HTML + // ID via Html::getUniqueId(), but here we bypass that in order to work + // around the data-drupal-selector bug. This is okay so long as we + // assume that this form only ever occurs once on a page. Remove this + // workaround in https://www.drupal.org/node/2897377. + $form['#id'] = Html::getId($form_state->getBuildInfo()['form_id']); + } + + // Mark this as an administrative page for JavaScript ("Back to site" link). + $form['#attached']['drupalSettings']['path']['currentPathIsAdmin'] = TRUE; + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + $subform_state = SubformState::createForSubform($form['layout_settings'], $form, $form_state); + $this->getPluginForm($this->layout)->validateConfigurationForm($form['layout_settings'], $subform_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + // Call the plugin submit handler. + $subform_state = SubformState::createForSubform($form['layout_settings'], $form, $form_state); + $this->getPluginForm($this->layout)->submitConfigurationForm($form['layout_settings'], $subform_state); + + // If this layout is context-aware, set the context mapping. + if ($this->layout instanceof ContextAwarePluginInterface) { + $this->layout->setContextMapping($subform_state->getValue('context_mapping', [])); + } + + $configuration = $this->layout->getConfiguration(); + + $section = $this->getCurrentSection(); + $section->setLayoutSettings($configuration); + if (!$this->isUpdate) { + $this->sectionStorage->insertSection($this->delta, $section); + } + + $this->layoutTempstoreRepository->set($this->sectionStorage); + $form_state->setRedirectUrl($this->sectionStorage->getLayoutBuilderUrl()); + } + + /** + * {@inheritdoc} + */ + protected function successfulAjaxSubmit(array $form, FormStateInterface $form_state) { + return $this->rebuildAndClose($this->sectionStorage); + } + + /** + * Retrieves the plugin form for a given layout. + * + * @param \Drupal\Core\Layout\LayoutInterface $layout + * The layout plugin. + * + * @return \Drupal\Core\Plugin\PluginFormInterface + * The plugin form for the layout. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + */ + protected function getPluginForm(LayoutInterface $layout) { + if ($layout instanceof PluginWithFormsInterface) { + return $this->pluginFormFactory->createInstance($layout, 'configure'); + } + + if ($layout instanceof PluginFormInterface) { + return $layout; + } + + throw new \InvalidArgumentException(sprintf('The "%s" layout does not provide a configuration form', $layout->getPluginId())); + } + + /** + * Retrieves the section storage property. + * + * @return \Drupal\layout_builder\SectionStorageInterface + * The section storage for the current form. + */ + public function getSectionStorage() { + return $this->sectionStorage; + } + + /** + * Retrieves the layout being modified by the form. + * + * @return \Drupal\Core\Layout\LayoutInterface|\Drupal\Core\Plugin\PluginFormInterface + * The layout for the current form. + */ + public function getCurrentLayout(): LayoutInterface { + return $this->layout; + } + + /** + * Retrieves the section being modified by the form. + * + * @return \Drupal\layout_builder\Section + * The section for the current form. + */ + public function getCurrentSection(): Section { + if (!isset($this->section)) { + if ($this->isUpdate) { + $this->section = $this->sectionStorage->getSection($this->delta); + } + else { + $this->section = new Section($this->pluginId); + } + } + + return $this->section; + } + +} diff --git a/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php index 1f62f0a38bb5..bd345041dc15 100644 --- a/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php +++ b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php @@ -288,8 +288,10 @@ public function testLayoutBuilderUi(): void { $expected_labels = [ 'My Cool Section', + 'Administration links for My Cool Section', 'Content region in My Cool Section', 'Section 2', + 'Administration links for Section 2', 'Content region in Section 2', ]; $labels = []; @@ -649,7 +651,177 @@ public function testCustomSectionAttributes(): void { $page->clickLink('Layout Builder Test Plugin'); $page->pressButton('Add section'); // See \Drupal\layout_builder_test\Plugin\Layout\LayoutBuilderTestPlugin::build(). - $assert_session->elementExists('css', '.go-birds'); + $assert_session->elementExists('css', 'div[aria-label="Section 1"] .go-birds'); + } + + /** + * Tests that editing sections keep custom attributes. + */ + public function testEditingSectionWithCustomAttributes() { + $assert_session = $this->assertSession(); + $page = $this->getSession()->getPage(); + + $this->drupalLogin($this->drupalCreateUser([ + 'configure any layout', + 'administer node display', + ])); + + $this->drupalGet('admin/structure/types/manage/bundle_with_section_field/display/default'); + $this->submitForm(['layout[enabled]' => TRUE], 'Save'); + $page->clickLink('Manage layout'); + $page->clickLink('Add section'); + $page->clickLink('Layout Builder Test Plugin'); + $page->pressButton('Add section'); + // See \Drupal\layout_builder_test\Plugin\Layout\LayoutBuilderTestPlugin::build(). + $assert_session->elementExists('css', 'div[aria-label="Section 1"] .go-birds'); + + $page->clickLink('Change layout for Section 1'); + $page->clickLink('Two column'); + $page->pressButton('Update'); + // TODO: Should we notify user? + $assert_session->elementExists('css', 'div[aria-label="Section 1"] .layout--twocol-section'); + $assert_session->elementNotExists('css', 'div[aria-label="Section 1"] .go-birds'); + } + + /** + * Tests the usage of placeholders for empty blocks. + * + * @see \Drupal\Core\Render\PreviewFallbackInterface::getPreviewFallbackString() + * @see \Drupal\layout_builder\EventSubscriber\BlockComponentRenderArray::onBuildRender() + */ + public function testBlockPlaceholder() { + $assert_session = $this->assertSession(); + $page = $this->getSession()->getPage(); + + $this->drupalLogin($this->drupalCreateUser([ + 'configure any layout', + 'administer node display', + ])); + + $field_ui_prefix = 'admin/structure/types/manage/bundle_with_section_field'; + $this->drupalGet("{$field_ui_prefix}/display/default"); + $this->submitForm(['layout[enabled]' => TRUE], 'Save'); + + // Customize the default view mode. + $this->drupalGet("$field_ui_prefix/display/default/layout"); + + // Add a block whose content is controlled by state and is empty by default. + $this->clickLink('Add block'); + $this->clickLink('Test block caching'); + $page->fillField('settings[label]', 'The block label'); + $page->pressButton('Add block'); + + $block_content = 'I am content'; + $placeholder_content = 'Placeholder for the "The block label" block'; + + // The block placeholder is displayed and there is no content. + $assert_session->pageTextContains($placeholder_content); + $assert_session->pageTextNotContains($block_content); + + // Set block content and reload the page. + \Drupal::state()->set('block_test.content', $block_content); + $this->getSession()->reload(); + + // The block placeholder is no longer displayed and the content is visible. + $assert_session->pageTextNotContains($placeholder_content); + $assert_session->pageTextContains($block_content); + } + + /** + * Tests the ability to use a specified block label for field blocks. + */ + public function testFieldBlockLabel() { + $assert_session = $this->assertSession(); + $page = $this->getSession()->getPage(); + + $this->drupalLogin($this->drupalCreateUser([ + 'configure any layout', + 'administer node display', + ])); + + $field_ui_prefix = 'admin/structure/types/manage/bundle_with_section_field'; + $this->drupalGet("$field_ui_prefix/display/default"); + $this->submitForm(['layout[enabled]' => TRUE], 'Save'); + + // Customize the default view mode. + $this->drupalGet("$field_ui_prefix/display/default/layout"); + + // Add a body block whose label will be overridden. + $this->clickLink('Add block'); + $this->clickLink('Body'); + + // Enable the Label Display and set the Label to a modified field + // block label. + $modified_field_block_label = 'Modified Field Block Label'; + $page->checkField('settings[label_display]'); + $page->fillField('settings[label]', $modified_field_block_label); + + // Save the block and layout. + $page->pressButton('Add block'); + $page->pressButton('Save layout'); + + // Revisit the default layout view mode page. + $this->drupalGet("$field_ui_prefix/display/default/layout"); + + // The modified field block label is displayed. + $assert_session->pageTextContains($modified_field_block_label); + } + + /** + * Tests a custom alter of the overrides form. + */ + public function testOverridesFormAlter() { + $assert_session = $this->assertSession(); + $page = $this->getSession()->getPage(); + + $this->drupalLogin($this->drupalCreateUser([ + 'configure any layout', + 'administer node display', + 'administer nodes', + ])); + + $field_ui_prefix = 'admin/structure/types/manage/bundle_with_section_field'; + // Enable overrides. + $this->drupalGet("{$field_ui_prefix}/display/default"); + $this->submitForm(['layout[enabled]' => TRUE], 'Save'); + $this->drupalGet("{$field_ui_prefix}/display/default"); + $this->submitForm(['layout[allow_custom]' => TRUE], 'Save'); + $this->drupalGet('node/1'); + + // The status checkbox should be checked by default. + $page->clickLink('Layout'); + $assert_session->checkboxChecked('status[value]'); + $page->pressButton('Save layout'); + $assert_session->pageTextContains('The layout override has been saved.'); + + // Unchecking the status checkbox will unpublish the entity. + $page->clickLink('Layout'); + $page->uncheckField('status[value]'); + $page->pressButton('Save layout'); + $assert_session->statusCodeEquals(403); + $assert_session->pageTextContains('The layout override has been saved.'); + } + + /** + * Tests the Block UI when Layout Builder is installed. + */ + public function testBlockUiListing() { + $assert_session = $this->assertSession(); + $page = $this->getSession()->getPage(); + + $this->drupalLogin($this->drupalCreateUser([ + 'administer blocks', + ])); + + $this->drupalGet('admin/structure/block'); + $page->clickLink('Place block'); + + // Ensure that blocks expected to appear are available. + $assert_session->pageTextContains('Test HTML block'); + $assert_session->pageTextContains('Block test'); + // Ensure that blocks not expected to appear are not available. + $assert_session->pageTextNotContains('Body'); + $assert_session->pageTextNotContains('Content fields'); } /** diff --git a/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderTest.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderTest.php index e5b81f7ddbd5..2a6d101489c2 100644 --- a/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderTest.php +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderTest.php @@ -226,6 +226,31 @@ public function testLayoutBuilderUi(): void { $page->pressButton('Save layout'); $assert_session->elementExists('css', '.layout'); + // Test editing the section + $this->drupalGet($layout_url); + $this->markCurrentPage(); + $assert_session->linkExists('Change layout for Section 2'); + $this->clickLink('Change layout for Section 2'); + $assert_session->addressEquals($layout_url); + $this->assertPageNotReloaded(); + + $this->assertNotEmpty($assert_session->waitForElementVisible('named', ['link', 'Two column'])); + + $this->clickLink('Two column'); + $this->assertOffCanvasFormAfterWait('layout_builder_configure_new_section_layout'); + $assert_session->pageTextContains('Configure new layout'); + $page->pressButton('Update'); + $assert_session->assertWaitOnAjaxRequest(); + + $assert_session->elementExists('css', 'div[aria-label="Second region in Section 2"]'); + $this->sortableTo('.block-field-blocknodebundle-with-section-fieldbody', + 'div[aria-label="First region in Section 2"].layout__region--first', + 'div[aria-label="Second region in Section 2"].layout__region--second'); + $assert_session->assertWaitOnAjaxRequest(); + + $assert_session->elementExists('css', 'div[aria-label="Second region in Section 2"].layout__region--second .block-field-blocknodebundle-with-section-fieldbody'); + $assert_session->elementTextContains('css', 'div[aria-label="Second region in Section 2"].layout__region--second', 'The node body'); + // Test deriver-based blocks. $this->drupalGet($layout_url); $this->markCurrentPage(); -- GitLab