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 682caa78c4..4b22b71281 100644 --- a/core/modules/layout_builder/config/schema/layout_builder.schema.yml +++ b/core/modules/layout_builder/config/schema/layout_builder.schema.yml @@ -2,6 +2,9 @@ core.entity_view_display.*.*.*.third_party.layout_builder: type: mapping label: 'Per-view-mode Layout Builder settings' mapping: + is_enabled: + type: boolean + label: 'Whether the Layout Builder is enabled for this display' allow_custom: type: boolean label: 'Allow a customized layout' diff --git a/core/modules/layout_builder/layout_builder.install b/core/modules/layout_builder/layout_builder.install index acb1e4fdf3..8081ee1ccc 100644 --- a/core/modules/layout_builder/layout_builder.install +++ b/core/modules/layout_builder/layout_builder.install @@ -13,6 +13,8 @@ * Implements hook_install(). */ function layout_builder_install() { + $display_changed = FALSE; + $displays = LayoutBuilderEntityViewDisplay::loadMultiple(); /** @var \Drupal\layout_builder\Entity\LayoutEntityDisplayInterface[] $displays */ foreach ($displays as $display) { @@ -20,21 +22,19 @@ function layout_builder_install() { $field_layout = $display->getThirdPartySettings('field_layout'); if (isset($field_layout['id'])) { $field_layout += ['settings' => []]; - $display->appendSection(new Section($field_layout['id'], $field_layout['settings'])); + $display + ->appendSection(new Section($field_layout['id'], $field_layout['settings'])) + ->enableLayoutBuilder() + ->save(); + $display_changed = TRUE; } - - // 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']); + if ($display_changed) { + Cache::invalidateTags(['rendered']); + } } diff --git a/core/modules/layout_builder/layout_builder.post_update.php b/core/modules/layout_builder/layout_builder.post_update.php index 9a2b85800e..a1024dc940 100644 --- a/core/modules/layout_builder/layout_builder.post_update.php +++ b/core/modules/layout_builder/layout_builder.post_update.php @@ -5,6 +5,9 @@ * Post update functions for Layout Builder. */ +use Drupal\Core\Config\Entity\ConfigEntityUpdater; +use Drupal\layout_builder\Entity\LayoutEntityDisplayInterface; + /** * Rebuild plugin dependencies for all entity view displays. */ @@ -24,3 +27,17 @@ function layout_builder_post_update_rebuild_plugin_dependencies(&$sandbox = NULL $sandbox['#finished'] = empty($sandbox['ids']) ? 1 : ($sandbox['count'] - count($sandbox['ids'])) / $sandbox['count']; } + +/** + * Enable Layout Builder for existing entity displays. + */ +function layout_builder_post_update_enable_existing(&$sandbox = NULL) { + $config_entity_updater = \Drupal::classResolver(ConfigEntityUpdater::class); + $config_entity_updater->update($sandbox, 'entity_view_display', function (LayoutEntityDisplayInterface $display) { + if ($display->getThirdPartySettings('layout_builder')) { + $display->enableLayoutBuilder(); + return TRUE; + } + return FALSE; + }); +} diff --git a/core/modules/layout_builder/layout_builder.routing.yml b/core/modules/layout_builder/layout_builder.routing.yml index 322e2301c1..54c9cf25b2 100644 --- a/core/modules/layout_builder/layout_builder.routing.yml +++ b/core/modules/layout_builder/layout_builder.routing.yml @@ -4,6 +4,7 @@ layout_builder.choose_section: _controller: '\Drupal\layout_builder\Controller\ChooseSectionController::build' requirements: _permission: 'configure any layout' + _layout_builder_access: 'view' options: _admin_route: TRUE parameters: @@ -16,6 +17,7 @@ layout_builder.add_section: _controller: '\Drupal\layout_builder\Controller\AddSectionController::build' requirements: _permission: 'configure any layout' + _layout_builder_access: 'view' options: _admin_route: TRUE parameters: @@ -32,6 +34,7 @@ layout_builder.configure_section: plugin_id: null requirements: _permission: 'configure any layout' + _layout_builder_access: 'view' options: _admin_route: TRUE parameters: @@ -44,6 +47,7 @@ layout_builder.remove_section: _form: '\Drupal\layout_builder\Form\RemoveSectionForm' requirements: _permission: 'configure any layout' + _layout_builder_access: 'view' options: _admin_route: TRUE parameters: @@ -56,6 +60,7 @@ layout_builder.choose_block: _controller: '\Drupal\layout_builder\Controller\ChooseBlockController::build' requirements: _permission: 'configure any layout' + _layout_builder_access: 'view' options: _admin_route: TRUE parameters: @@ -68,6 +73,7 @@ layout_builder.add_block: _form: '\Drupal\layout_builder\Form\AddBlockForm' requirements: _permission: 'configure any layout' + _layout_builder_access: 'view' options: _admin_route: TRUE parameters: @@ -80,6 +86,7 @@ layout_builder.update_block: _form: '\Drupal\layout_builder\Form\UpdateBlockForm' requirements: _permission: 'configure any layout' + _layout_builder_access: 'view' options: _admin_route: TRUE parameters: @@ -92,6 +99,7 @@ layout_builder.remove_block: _form: '\Drupal\layout_builder\Form\RemoveBlockForm' requirements: _permission: 'configure any layout' + _layout_builder_access: 'view' options: _admin_route: TRUE parameters: @@ -110,6 +118,7 @@ layout_builder.move_block: preceding_block_uuid: null requirements: _permission: 'configure any layout' + _layout_builder_access: 'view' options: _admin_route: TRUE parameters: diff --git a/core/modules/layout_builder/layout_builder.services.yml b/core/modules/layout_builder/layout_builder.services.yml index 4fe50929bd..f2360a50ff 100644 --- a/core/modules/layout_builder/layout_builder.services.yml +++ b/core/modules/layout_builder/layout_builder.services.yml @@ -2,6 +2,10 @@ services: layout_builder.tempstore_repository: class: Drupal\layout_builder\LayoutTempstoreRepository arguments: ['@tempstore.shared'] + access_check.entity.layout_builder_access: + class: Drupal\layout_builder\Access\LayoutBuilderAccessCheck + tags: + - { name: access_check, applies_to: _layout_builder_access } access_check.entity.layout: class: Drupal\layout_builder\Access\LayoutSectionAccessCheck tags: diff --git a/core/modules/layout_builder/src/Access/LayoutBuilderAccessCheck.php b/core/modules/layout_builder/src/Access/LayoutBuilderAccessCheck.php new file mode 100644 index 0000000000..0a70db0633 --- /dev/null +++ b/core/modules/layout_builder/src/Access/LayoutBuilderAccessCheck.php @@ -0,0 +1,40 @@ +getRequirement('_layout_builder_access'); + $access = $section_storage->access($operation, $account, TRUE); + if ($access instanceof RefinableCacheableDependencyInterface) { + $access->addCacheableDependency($section_storage); + } + return $access; + } + +} diff --git a/core/modules/layout_builder/src/DefaultsSectionStorageInterface.php b/core/modules/layout_builder/src/DefaultsSectionStorageInterface.php index 121d9c84eb..0bcaa387e0 100644 --- a/core/modules/layout_builder/src/DefaultsSectionStorageInterface.php +++ b/core/modules/layout_builder/src/DefaultsSectionStorageInterface.php @@ -11,8 +11,10 @@ * 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. + * + * @todo Refactor this interface in https://www.drupal.org/node/2985362. */ -interface DefaultsSectionStorageInterface extends SectionStorageInterface, ThirdPartySettingsInterface { +interface DefaultsSectionStorageInterface extends SectionStorageInterface, ThirdPartySettingsInterface, LayoutBuilderEnabledInterface { /** * Determines if the defaults allow custom overrides. diff --git a/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php index 4df6afe89a..1647132ea1 100644 --- a/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php +++ b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php @@ -37,10 +37,65 @@ public function isOverridable() { * {@inheritdoc} */ public function setOverridable($overridable = TRUE) { + if ($overridable) { + $this->enableLayoutBuilder(); + } $this->setThirdPartySetting('layout_builder', 'allow_custom', $overridable); return $this; } + /** + * {@inheritdoc} + */ + public function isLayoutBuilderEnabled() { + return (bool) $this->getThirdPartySetting('layout_builder', 'is_enabled'); + } + + /** + * {@inheritdoc} + */ + public function enableLayoutBuilder() { + // Initialize Layout Builder if it is being enabled for the first time. + if (!$this->isLayoutBuilderEnabled()) { + $this->initializeLayoutBuilder(); + } + + $this->setThirdPartySetting('layout_builder', 'is_enabled', TRUE); + return $this; + } + + /** + * Initializes Layout Builder when being enabled. + * + * @return $this + */ + protected function initializeLayoutBuilder() { + // Loop through all existing field-based components and add them as + // section-based components. + $components = $this->getComponents(); + // Sort the components by weight. + uasort($components, 'Drupal\Component\Utility\SortArray::sortByWeightElement'); + foreach ($components as $name => $component) { + $this->setComponent($name, $component); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function disableLayoutBuilder() { + // If Layout Builder is being disabled, remove all existing section data. + if ($this->isLayoutBuilderEnabled()) { + while (count($this) > 0) { + $this->removeSection(0); + } + } + $this->setThirdPartySetting('layout_builder', 'is_enabled', FALSE); + return $this; + } + /** * {@inheritdoc} */ @@ -136,6 +191,9 @@ protected function contextRepository() { */ public function buildMultiple(array $entities) { $build_list = parent::buildMultiple($entities); + if (!$this->isLayoutBuilderEnabled()) { + return $build_list; + } foreach ($entities as $id => $entity) { $sections = $this->getRuntimeSections($entity); diff --git a/core/modules/layout_builder/src/Entity/LayoutEntityDisplayInterface.php b/core/modules/layout_builder/src/Entity/LayoutEntityDisplayInterface.php index b8fd21d822..51151bd81c 100644 --- a/core/modules/layout_builder/src/Entity/LayoutEntityDisplayInterface.php +++ b/core/modules/layout_builder/src/Entity/LayoutEntityDisplayInterface.php @@ -3,6 +3,7 @@ namespace Drupal\layout_builder\Entity; use Drupal\Core\Entity\Display\EntityDisplayInterface; +use Drupal\layout_builder\LayoutBuilderEnabledInterface; use Drupal\layout_builder\SectionListInterface; /** @@ -12,8 +13,10 @@ * 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. + * + * @todo Refactor this interface in https://www.drupal.org/node/2985362. */ -interface LayoutEntityDisplayInterface extends EntityDisplayInterface, SectionListInterface { +interface LayoutEntityDisplayInterface extends EntityDisplayInterface, SectionListInterface, LayoutBuilderEnabledInterface { /** * Determines if the display allows custom overrides. diff --git a/core/modules/layout_builder/src/Form/LayoutBuilderDisableForm.php b/core/modules/layout_builder/src/Form/LayoutBuilderDisableForm.php new file mode 100644 index 0000000000..48b42b740d --- /dev/null +++ b/core/modules/layout_builder/src/Form/LayoutBuilderDisableForm.php @@ -0,0 +1,106 @@ +layoutTempstoreRepository = $layout_tempstore_repository; + $this->setMessenger($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_disable_form'; + } + + /** + * {@inheritdoc} + */ + public function getQuestion() { + return $this->t('Are you sure you want to disable Layout Builder?'); + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + return $this->t('All customizations will be removed. This action cannot be undone.'); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + return $this->sectionStorage->getRedirectUrl(); + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state, SectionStorageInterface $section_storage = NULL) { + if (!$section_storage instanceof DefaultsSectionStorageInterface) { + throw new \InvalidArgumentException(sprintf('The section storage with type "%s" and ID "%s" does not provide defaults', $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) { + $this->sectionStorage->disableLayoutBuilder()->save(); + $this->layoutTempstoreRepository->delete($this->sectionStorage); + + $this->messenger()->addMessage($this->t('Layout Builder has been disabled.')); + $form_state->setRedirectUrl($this->getCancelUrl()); + } + +} diff --git a/core/modules/layout_builder/src/Form/LayoutBuilderEntityViewDisplayForm.php b/core/modules/layout_builder/src/Form/LayoutBuilderEntityViewDisplayForm.php index 63ec398eec..15d3653ce1 100644 --- a/core/modules/layout_builder/src/Form/LayoutBuilderEntityViewDisplayForm.php +++ b/core/modules/layout_builder/src/Form/LayoutBuilderEntityViewDisplayForm.php @@ -28,7 +28,7 @@ class LayoutBuilderEntityViewDisplayForm extends EntityViewDisplayEditForm { /** * The storage section. * - * @var \Drupal\layout_builder\SectionStorageInterface + * @var \Drupal\layout_builder\DefaultsSectionStorageInterface */ protected $sectionStorage; @@ -46,10 +46,17 @@ public function buildForm(array $form, FormStateInterface $form_state, SectionSt public function form(array $form, FormStateInterface $form_state) { $form = parent::form($form, $form_state); - // Hide the table of fields. - $form['fields']['#access'] = FALSE; - $form['#fields'] = []; - $form['#extra'] = []; + $is_enabled = $this->entity->isLayoutBuilderEnabled(); + if ($is_enabled) { + // Hide the table of fields. + $form['fields']['#access'] = FALSE; + $form['#fields'] = []; + $form['#extra'] = []; + } + else { + // Remove the Layout Builder field from the list. + $form['#fields'] = array_diff($form['#fields'], ['layout_builder__layout']); + } $form['manage_layout'] = [ '#type' => 'link', @@ -57,18 +64,26 @@ public function form(array $form, FormStateInterface $form_state) { '#weight' => -10, '#attributes' => ['class' => ['button']], '#url' => $this->sectionStorage->getLayoutBuilderUrl(), + '#access' => $is_enabled, ]; + $form['layout'] = [ + '#type' => 'details', + '#open' => TRUE, + '#title' => $this->t('Layout options'), + '#tree' => TRUE, + ]; + + $form['layout']['is_enabled'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Use Layout Builder'), + '#default_value' => $is_enabled, + ]; + $form['#entity_builders']['layout_builder'] = '::entityFormEntityBuild'; + // @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()); $form['layout']['allow_custom'] = [ '#type' => 'checkbox', @@ -76,14 +91,23 @@ public function form(array $form, FormStateInterface $form_state) { '@entity' => $entity_type->getSingularLabel(), ]), '#default_value' => $this->entity->isOverridable(), + '#disabled' => !$is_enabled, + '#states' => [ + 'disabled' => [ + ':input[name="layout[is_enabled]"]' => ['checked' => FALSE], + ], + 'invisible' => [ + ':input[name="layout[is_enabled]"]' => ['checked' => FALSE], + ], + ], ]; // Prevent turning off overrides while any exist. if ($this->hasOverrides($this->entity)) { + $form['layout']['is_enabled']['#disabled'] = TRUE; + $form['layout']['is_enabled']['#description'] = $this->t('You must revert all customized layouts of this display before you can disable this option.'); $form['layout']['allow_custom']['#disabled'] = TRUE; $form['layout']['allow_custom']['#description'] = $this->t('You must revert all customized layouts of this display before you can disable this option.'); - } - else { - $form['#entity_builders'][] = '::entityFormEntityBuild'; + unset($form['#entity_builders']['layout_builder']); } } return $form; @@ -116,22 +140,43 @@ protected function hasOverrides(LayoutEntityDisplayInterface $display) { * 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); + $set_enabled = (bool) $form_state->getValue(['layout', 'is_enabled'], FALSE); + if ($set_enabled) { + $overridable = (bool) $form_state->getValue(['layout', 'allow_custom'], FALSE); + } + else { + if ($display->isLayoutBuilderEnabled()) { + $form_state->setRedirectUrl($this->sectionStorage->getLayoutBuilderUrl('disable')); + return; + } + $overridable = FALSE; + } + + if ($set_enabled) { + $display->enableLayoutBuilder(); + } + else { + $display->disableLayoutBuilder(); + } + $display->setOverridable($overridable); } /** * {@inheritdoc} */ protected function buildFieldRow(FieldDefinitionInterface $field_definition, array $form, FormStateInterface $form_state) { - // Intentionally empty. + if (!$this->entity->isLayoutBuilderEnabled() && $field_definition->getType() !== 'layout_section') { + return parent::buildFieldRow($field_definition, $form, $form_state); + } } /** * {@inheritdoc} */ protected function buildExtraFieldRow($field_id, $extra_field) { - // Intentionally empty. + if (!$this->entity->isLayoutBuilderEnabled()) { + return parent::buildExtraFieldRow($field_id, $extra_field); + } } } diff --git a/core/modules/layout_builder/src/LayoutBuilderEnabledInterface.php b/core/modules/layout_builder/src/LayoutBuilderEnabledInterface.php new file mode 100644 index 0000000000..dab8fc4bef --- /dev/null +++ b/core/modules/layout_builder/src/LayoutBuilderEnabledInterface.php @@ -0,0 +1,32 @@ +getStorageType()}.{$this->getDisplay()->getTargetEntityTypeId()}.view", $this->getRouteParameters()); + public function getLayoutBuilderUrl($rel = 'view') { + return Url::fromRoute("layout_builder.{$this->getStorageType()}.{$this->getDisplay()->getTargetEntityTypeId()}.$rel", $this->getRouteParameters()); } /** @@ -293,6 +295,29 @@ public function setOverridable($overridable = TRUE) { return $this; } + /** + * {@inheritdoc} + */ + public function isLayoutBuilderEnabled() { + return $this->getDisplay()->isLayoutBuilderEnabled(); + } + + /** + * {@inheritdoc} + */ + public function enableLayoutBuilder() { + $this->getDisplay()->enableLayoutBuilder(); + return $this; + } + + /** + * {@inheritdoc} + */ + public function disableLayoutBuilder() { + $this->getDisplay()->disableLayoutBuilder(); + return $this; + } + /** * {@inheritdoc} */ @@ -330,4 +355,12 @@ public function getThirdPartyProviders() { return $this->getDisplay()->getThirdPartyProviders(); } + /** + * {@inheritdoc} + */ + public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) { + $result = AccessResult::allowedIf($this->isLayoutBuilderEnabled()); + return $return_as_object ? $result : $result->isAllowed(); + } + } diff --git a/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php index db38b1203e..c38ec5e1d0 100644 --- a/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php @@ -2,6 +2,7 @@ namespace Drupal\layout_builder\Plugin\SectionStorage; +use Drupal\Core\Access\AccessResult; use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; @@ -10,6 +11,7 @@ use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Plugin\Context\Context; use Drupal\Core\Plugin\Context\ContextDefinition; +use Drupal\Core\Session\AccountInterface; use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\Core\Url; use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay; @@ -202,10 +204,10 @@ public function getRedirectUrl() { /** * {@inheritdoc} */ - public function getLayoutBuilderUrl() { + public function getLayoutBuilderUrl($rel = 'view') { $entity = $this->getEntity(); $route_parameters[$entity->getEntityTypeId()] = $entity->id(); - return Url::fromRoute("layout_builder.{$this->getStorageType()}.{$this->getEntity()->getEntityTypeId()}.view", $route_parameters); + return Url::fromRoute("layout_builder.{$this->getStorageType()}.{$this->getEntity()->getEntityTypeId()}.$rel", $route_parameters); } /** @@ -233,4 +235,13 @@ public function save() { return $this->getEntity()->save(); } + /** + * {@inheritdoc} + */ + public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) { + $default_section_storage = $this->getDefaultSectionStorage(); + $result = AccessResult::allowedIf($default_section_storage->isLayoutBuilderEnabled())->addCacheableDependency($default_section_storage); + return $return_as_object ? $result : $result->isAllowed(); + } + } diff --git a/core/modules/layout_builder/src/Routing/LayoutBuilderRoutesTrait.php b/core/modules/layout_builder/src/Routing/LayoutBuilderRoutesTrait.php index febedbe31e..d06b95324b 100644 --- a/core/modules/layout_builder/src/Routing/LayoutBuilderRoutesTrait.php +++ b/core/modules/layout_builder/src/Routing/LayoutBuilderRoutesTrait.php @@ -3,6 +3,7 @@ namespace Drupal\layout_builder\Routing; use Drupal\Component\Utility\NestedArray; +use Drupal\layout_builder\DefaultsSectionStorageInterface; use Drupal\layout_builder\OverridesSectionStorageInterface; use Drupal\layout_builder\SectionStorage\SectionStorageDefinition; use Symfony\Component\Routing\Route; @@ -43,6 +44,7 @@ protected function buildLayoutRoutes(RouteCollection $collection, SectionStorage $defaults['section_storage'] = ''; // Trigger the layout builder access check. $requirements['_has_layout_section'] = 'true'; + $requirements['_layout_builder_access'] = 'view'; // Trigger the layout builder RouteEnhancer. $options['_layout_builder'] = TRUE; // Trigger the layout builder param converter. @@ -92,6 +94,17 @@ protected function buildLayoutRoutes(RouteCollection $collection, SectionStorage ->setOptions($options); $collection->add("$route_name_prefix.revert", $route); } + elseif (is_subclass_of($definition->getClass(), DefaultsSectionStorageInterface::class)) { + $disable_defaults = $defaults; + $disable_defaults['_form'] = '\Drupal\layout_builder\Form\LayoutBuilderDisableForm'; + $disable_options = $options; + unset($disable_options['_admin_route'], $disable_options['_layout_builder']); + $route = (new Route("$path/disable")) + ->setDefaults($disable_defaults) + ->setRequirements($requirements) + ->setOptions($disable_options); + $collection->add("$route_name_prefix.disable", $route); + } } } diff --git a/core/modules/layout_builder/src/SectionStorageInterface.php b/core/modules/layout_builder/src/SectionStorageInterface.php index e7fbb086b8..90ce9072fd 100644 --- a/core/modules/layout_builder/src/SectionStorageInterface.php +++ b/core/modules/layout_builder/src/SectionStorageInterface.php @@ -3,6 +3,7 @@ namespace Drupal\layout_builder; use Drupal\Component\Plugin\PluginInspectionInterface; +use Drupal\Core\Access\AccessibleInterface; use Symfony\Component\Routing\RouteCollection; /** @@ -13,7 +14,7 @@ * experimental modules and development releases of contributed modules. * See https://www.drupal.org/core/experimental for more information. */ -interface SectionStorageInterface extends SectionListInterface, PluginInspectionInterface { +interface SectionStorageInterface extends SectionListInterface, PluginInspectionInterface, AccessibleInterface { /** * Returns an identifier for this storage. @@ -88,10 +89,14 @@ public function getRedirectUrl(); /** * Gets the URL used to display the Layout Builder UI. * + * @param string $rel + * (optional) The link relationship type, for example: 'view' or 'disable'. + * Defaults to 'view'. + * * @return \Drupal\Core\Url * The URL object. */ - public function getLayoutBuilderUrl(); + public function getLayoutBuilderUrl($rel = 'view'); /** * Configures the plugin based on route values. diff --git a/core/modules/layout_builder/tests/src/Functional/LayoutBuilderOptInTest.php b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderOptInTest.php new file mode 100644 index 0000000000..07641217f6 --- /dev/null +++ b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderOptInTest.php @@ -0,0 +1,132 @@ +drupalPlaceBlock('local_tasks_block'); + + // Create one content type before installing Layout Builder and one after. + $this->createContentType(['type' => 'before']); + $this->container->get('module_installer')->install(['layout_builder']); + $this->rebuildAll(); + $this->createContentType(['type' => 'after']); + + $this->drupalLogin($this->drupalCreateUser([ + 'configure any layout', + 'administer node display', + ])); + } + + /** + * Tests the expected default values for enabling Layout Builder. + */ + public function testDefaultValues() { + $assert_session = $this->assertSession(); + $page = $this->getSession()->getPage(); + + // Both the content type created before and after Layout Builder was + // installed is still using the Field UI. + $this->drupalGet('admin/structure/types/manage/before/display/default'); + $assert_session->checkboxNotChecked('layout[is_enabled]'); + + $field_ui_prefix = 'admin/structure/types/manage/after/display/default'; + $this->drupalGet($field_ui_prefix); + $assert_session->checkboxNotChecked('layout[is_enabled]'); + $page->checkField('layout[is_enabled]'); + $page->pressButton('Save'); + + $layout_builder_ui = $this->getPathForFieldBlock('node', 'after', 'default', 'body'); + + $assert_session->linkExists('Manage layout'); + $this->clickLink('Manage layout'); + + // Change the body formatter to Trimmed. + $this->drupalGet($layout_builder_ui); + $assert_session->fieldValueEquals('settings[formatter][type]', 'text_default'); + $page->selectFieldOption('settings[formatter][type]', 'text_trimmed'); + $page->pressButton('Update'); + $assert_session->linkExists('Save Layout'); + $this->clickLink('Save Layout'); + + $this->drupalGet($layout_builder_ui); + $assert_session->fieldValueEquals('settings[formatter][type]', 'text_trimmed'); + + // Disable Layout Builder. + $this->drupalPostForm($field_ui_prefix, ['layout[is_enabled]' => FALSE], 'Save'); + $page->pressButton('Confirm'); + + // The Layout Builder UI is no longer accessible. + $this->drupalGet($layout_builder_ui); + $assert_session->statusCodeEquals(403); + + // The original body formatter is reflected in Field UI. + $this->drupalGet($field_ui_prefix); + $assert_session->fieldValueEquals('fields[body][type]', 'text_default'); + + // Change the body formatter to Summary. + $page->selectFieldOption('fields[body][type]', 'text_summary_or_trimmed'); + $page->pressButton('Save'); + $assert_session->fieldValueEquals('fields[body][type]', 'text_summary_or_trimmed'); + + // Reactivate Layout Builder. + $this->drupalPostForm($field_ui_prefix, ['layout[is_enabled]' => TRUE], 'Save'); + // The changed body formatter is reflected in Layout Builder UI. + $this->drupalGet($this->getPathForFieldBlock('node', 'after', 'default', 'body')); + $assert_session->fieldValueEquals('settings[formatter][type]', 'text_summary_or_trimmed'); + } + + /** + * Returns the path to update a field block in the UI. + * + * @param string $entity_type_id + * The entity type ID. + * @param string $bundle + * The bundle. + * @param string $view_mode + * The view mode. + * @param string $field_name + * The field name. + * + * @return string + * The path. + */ + protected function getPathForFieldBlock($entity_type_id, $bundle, $view_mode, $field_name) { + $delta = 0; + /** @var \Drupal\layout_builder\Entity\LayoutEntityDisplayInterface $display */ + $display = $this->container->get('entity_type.manager')->getStorage('entity_view_display')->load("$entity_type_id.$bundle.$view_mode"); + $body_component = NULL; + foreach ($display->getSection($delta)->getComponents() as $component) { + if ($component->getPluginId() === "field_block:$entity_type_id:$bundle:$field_name") { + $body_component = $component; + } + } + $this->assertNotNull($body_component); + return 'layout_builder/update/block/defaults/node.after.default/0/content/' . $body_component->getUuid(); + } + +} diff --git a/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php index 62429573a4..7d79c23894 100644 --- a/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php +++ b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php @@ -80,6 +80,10 @@ public function testLayoutBuilderUi() { // From the manage display page, go to manage the layout. $this->drupalGet("$field_ui_prefix/display/default"); + $assert_session->linkNotExists('Manage layout'); + $assert_session->fieldDisabled('layout[allow_custom]'); + + $this->drupalPostForm(NULL, ['layout[is_enabled]' => TRUE], 'Save'); $assert_session->linkExists('Manage layout'); $this->clickLink('Manage layout'); $assert_session->addressEquals("$field_ui_prefix/display-layout/default"); @@ -131,6 +135,7 @@ public function testLayoutBuilderUi() { // Assert that overrides cannot be turned off while overrides exist. $this->drupalGet("$field_ui_prefix/display/default"); + $assert_session->checkboxChecked('layout[allow_custom]'); $assert_session->fieldDisabled('layout[allow_custom]'); // Alter the defaults. @@ -216,7 +221,9 @@ public function testPluginDependencies() { $page->fillField('id', 'myothermenu'); $page->pressButton('Save'); - $this->drupalGet('admin/structure/types/manage/bundle_with_section_field/display-layout/default'); + $this->drupalPostForm('admin/structure/types/manage/bundle_with_section_field/display', ['layout[is_enabled]' => TRUE], 'Save'); + $assert_session->linkExists('Manage layout'); + $this->clickLink('Manage layout'); $assert_session->linkExists('Add Section'); $this->clickLink('Add Section'); $assert_session->linkExists('Layout plugin (with dependencies)'); @@ -278,6 +285,7 @@ public function testLayoutBuilderUiFullViewMode() { $field_ui_prefix = 'admin/structure/types/manage/bundle_with_section_field'; // Allow overrides for the layout. + $this->drupalPostForm("$field_ui_prefix/display/default", ['layout[is_enabled]' => TRUE], 'Save'); $this->drupalPostForm("$field_ui_prefix/display/default", ['layout[allow_custom]' => TRUE], 'Save'); // Customize the default view mode. @@ -338,7 +346,8 @@ public function testLayoutBuilderChooseBlocksAlter() { ])); // From the manage display page, go to manage the layout. - $this->drupalGet('admin/structure/types/manage/bundle_with_section_field/display/default'); + $this->drupalPostForm('admin/structure/types/manage/bundle_with_section_field/display/default', ['layout[is_enabled]' => TRUE], 'Save'); + $assert_session->linkExists('Manage layout'); $this->clickLink('Manage layout'); // Add a new block. @@ -385,6 +394,7 @@ public function testDeletedView() { $field_ui_prefix = 'admin/structure/types/manage/bundle_with_section_field'; // Enable overrides. + $this->drupalPostForm("$field_ui_prefix/display/default", ['layout[is_enabled]' => TRUE], 'Save'); $this->drupalPostForm("$field_ui_prefix/display/default", ['layout[allow_custom]' => TRUE], 'Save'); $this->drupalGet('node/1'); diff --git a/core/modules/layout_builder/tests/src/FunctionalJavascript/AjaxBlockTest.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/AjaxBlockTest.php index 1f7849ef9a..289ee646fa 100644 --- a/core/modules/layout_builder/tests/src/FunctionalJavascript/AjaxBlockTest.php +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/AjaxBlockTest.php @@ -60,7 +60,7 @@ public function testAddAjaxBlock() { $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"); + $this->drupalPostForm("$field_ui_prefix/display/default", ['layout[is_enabled]' => TRUE], 'Save'); $assert_session->linkExists('Manage layout'); $this->clickLink('Manage layout'); $assert_session->addressEquals("$field_ui_prefix/display-layout/default"); diff --git a/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderCompatibilityTestBase.php b/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderCompatibilityTestBase.php index d5a4cd0b8a..e3868dbff0 100644 --- a/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderCompatibilityTestBase.php +++ b/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderCompatibilityTestBase.php @@ -86,13 +86,19 @@ protected function setUp() { /** * Installs the Layout Builder. * - * Also configures and reloads the entity display, and reloads the entity. + * Also configures and reloads the entity display. */ protected function installLayoutBuilder() { $this->container->get('module_installer')->install(['layout_builder']); $this->refreshServices(); $this->display = $this->reloadEntity($this->display); + } + + /** + * Enables overrides for the display and reloads the entity. + */ + protected function enableOverrides() { $this->display->setOverridable()->save(); $this->entity = $this->reloadEntity($this->entity); } diff --git a/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderFieldLayoutCompatibilityTest.php b/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderFieldLayoutCompatibilityTest.php index 59a76615c1..57dacb5b18 100644 --- a/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderFieldLayoutCompatibilityTest.php +++ b/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderFieldLayoutCompatibilityTest.php @@ -54,6 +54,7 @@ public function testCompatibility() { $this->assertFieldAttributes($this->entity, $expected_fields); // Add a layout override. + $this->enableOverrides(); /** @var \Drupal\layout_builder\SectionStorageInterface $field_list */ $field_list = $this->entity->get('layout_builder__layout'); $field_list->appendSection(new Section('layout_onecol')); diff --git a/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderInstallTest.php b/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderInstallTest.php index fa646df1c9..aa1b9942c8 100644 --- a/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderInstallTest.php +++ b/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderInstallTest.php @@ -33,6 +33,8 @@ public function testCompatibility() { $this->assertFieldAttributes($this->entity, $expected_fields); // Add a layout override. + $this->enableOverrides(); + $this->entity = $this->reloadEntity($this->entity); $this->entity->get('layout_builder__layout')->appendSection(new Section('layout_onecol')); $this->entity->save(); diff --git a/core/modules/layout_builder/tests/src/Unit/DefaultsSectionStorageTest.php b/core/modules/layout_builder/tests/src/Unit/DefaultsSectionStorageTest.php index 7e5304dbbe..d30917a446 100644 --- a/core/modules/layout_builder/tests/src/Unit/DefaultsSectionStorageTest.php +++ b/core/modules/layout_builder/tests/src/Unit/DefaultsSectionStorageTest.php @@ -254,6 +254,7 @@ public function testBuildRoutes() { [ '_field_ui_view_mode_access' => 'administer with_bundle_key display', '_has_layout_section' => 'true', + '_layout_builder_access' => 'view', ], [ 'parameters' => [ @@ -275,6 +276,7 @@ public function testBuildRoutes() { [ '_field_ui_view_mode_access' => 'administer with_bundle_key display', '_has_layout_section' => 'true', + '_layout_builder_access' => 'view', ], [ 'parameters' => [ @@ -296,6 +298,7 @@ public function testBuildRoutes() { [ '_field_ui_view_mode_access' => 'administer with_bundle_key display', '_has_layout_section' => 'true', + '_layout_builder_access' => 'view', ], [ 'parameters' => [ @@ -305,6 +308,26 @@ public function testBuildRoutes() { '_admin_route' => FALSE, ] ), + 'layout_builder.defaults.with_bundle_key.disable' => new Route( + '/admin/entity/whatever/display-layout/{view_mode_name}/disable', + [ + 'entity_type_id' => 'with_bundle_key', + 'bundle_key' => 'my_bundle_type', + 'section_storage_type' => 'defaults', + 'section_storage' => '', + '_form' => '\Drupal\layout_builder\Form\LayoutBuilderDisableForm', + ], + [ + '_field_ui_view_mode_access' => 'administer with_bundle_key display', + '_has_layout_section' => 'true', + '_layout_builder_access' => 'view', + ], + [ + 'parameters' => [ + 'section_storage' => ['layout_builder_tempstore' => TRUE], + ], + ] + ), 'layout_builder.defaults.with_bundle_parameter.view' => new Route( '/admin/entity/{bundle}/display-layout/{view_mode_name}', [ @@ -318,6 +341,7 @@ public function testBuildRoutes() { [ '_field_ui_view_mode_access' => 'administer with_bundle_parameter display', '_has_layout_section' => 'true', + '_layout_builder_access' => 'view', ], [ 'parameters' => [ @@ -338,6 +362,7 @@ public function testBuildRoutes() { [ '_field_ui_view_mode_access' => 'administer with_bundle_parameter display', '_has_layout_section' => 'true', + '_layout_builder_access' => 'view', ], [ 'parameters' => [ @@ -358,6 +383,7 @@ public function testBuildRoutes() { [ '_field_ui_view_mode_access' => 'administer with_bundle_parameter display', '_has_layout_section' => 'true', + '_layout_builder_access' => 'view', ], [ 'parameters' => [ @@ -367,6 +393,25 @@ public function testBuildRoutes() { '_admin_route' => FALSE, ] ), + 'layout_builder.defaults.with_bundle_parameter.disable' => new Route( + '/admin/entity/{bundle}/display-layout/{view_mode_name}/disable', + [ + 'entity_type_id' => 'with_bundle_parameter', + 'section_storage_type' => 'defaults', + 'section_storage' => '', + '_form' => '\Drupal\layout_builder\Form\LayoutBuilderDisableForm', + ], + [ + '_field_ui_view_mode_access' => 'administer with_bundle_parameter display', + '_has_layout_section' => 'true', + '_layout_builder_access' => 'view', + ], + [ + 'parameters' => [ + 'section_storage' => ['layout_builder_tempstore' => TRUE], + ], + ] + ), ]; $collection = new RouteCollection(); diff --git a/core/modules/layout_builder/tests/src/Unit/OverridesSectionStorageTest.php b/core/modules/layout_builder/tests/src/Unit/OverridesSectionStorageTest.php index bbeb049a61..110ec2021e 100644 --- a/core/modules/layout_builder/tests/src/Unit/OverridesSectionStorageTest.php +++ b/core/modules/layout_builder/tests/src/Unit/OverridesSectionStorageTest.php @@ -223,6 +223,7 @@ public function testBuildRoutes() { ], [ '_has_layout_section' => 'true', + '_layout_builder_access' => 'view', ], [ 'parameters' => [ @@ -242,6 +243,7 @@ public function testBuildRoutes() { ], [ '_has_layout_section' => 'true', + '_layout_builder_access' => 'view', ], [ 'parameters' => [ @@ -261,6 +263,7 @@ public function testBuildRoutes() { ], [ '_has_layout_section' => 'true', + '_layout_builder_access' => 'view', ], [ 'parameters' => [ @@ -280,6 +283,7 @@ public function testBuildRoutes() { ], [ '_has_layout_section' => 'true', + '_layout_builder_access' => 'view', ], [ 'parameters' => [ @@ -301,6 +305,7 @@ public function testBuildRoutes() { ], [ '_has_layout_section' => 'true', + '_layout_builder_access' => 'view', 'with_integer_id' => '\d+', ], [ @@ -321,6 +326,7 @@ public function testBuildRoutes() { ], [ '_has_layout_section' => 'true', + '_layout_builder_access' => 'view', 'with_integer_id' => '\d+', ], [ @@ -341,6 +347,7 @@ public function testBuildRoutes() { ], [ '_has_layout_section' => 'true', + '_layout_builder_access' => 'view', 'with_integer_id' => '\d+', ], [ @@ -361,6 +368,7 @@ public function testBuildRoutes() { ], [ '_has_layout_section' => 'true', + '_layout_builder_access' => 'view', 'with_integer_id' => '\d+', ], [