Unverified Commit c6cdc439 authored by alexpott's avatar alexpott

Issue #2936358 by tim.plunkett, johndevman, tedbow, eiriksm, AaronMcHale,...

Issue #2936358 by tim.plunkett, johndevman, tedbow, eiriksm, AaronMcHale, phenaproxima, pookmish, alexpott: Layout Builder should be opt-in per display (entity type/bundle/view mode)
parent df94b0c1
......@@ -2,6 +2,9 @@ core.entity_view_display.*.*.*.third_party.layout_builder:
type: mapping
label: 'Per-view-mode Layout Builder settings'
mapping:
enabled:
type: boolean
label: 'Whether the Layout Builder is enabled for this display'
allow_custom:
type: boolean
label: 'Allow a customized layout'
......
......@@ -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,43 @@ 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']));
}
// 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
->enableLayoutBuilder()
->appendSection(new Section($field_layout['id'], $field_layout['settings']))
->save();
$display_changed = TRUE;
}
$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']);
}
}
/**
* Enable Layout Builder for existing entity displays.
*/
function layout_builder_update_8601(&$sandbox) {
$config_factory = \Drupal::configFactory();
if (!isset($sandbox['count'])) {
$sandbox['ids'] = $config_factory->listAll('core.entity_view_display.');
$sandbox['count'] = count($sandbox['ids']);
}
$ids = array_splice($sandbox['ids'], 0, 50);
foreach ($ids as $id) {
$display = $config_factory->getEditable($id);
if ($display->get('third_party_settings.layout_builder')) {
$display
->set('third_party_settings.layout_builder.enabled', TRUE)
->save();
}
}
$sandbox['#finished'] = empty($sandbox['ids']) ? 1 : ($sandbox['count'] - count($sandbox['ids'])) / $sandbox['count'];
}
......@@ -6,7 +6,7 @@
*/
use Drupal\Core\Config\Entity\ConfigEntityUpdater;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\layout_builder\Entity\LayoutEntityDisplayInterface;
/**
* Rebuild plugin dependencies for all entity view displays.
......@@ -39,7 +39,11 @@ function layout_builder_post_update_rebuild_plugin_dependencies(&$sandbox = NULL
*/
function layout_builder_post_update_add_extra_fields(&$sandbox = NULL) {
$entity_field_manager = \Drupal::service('entity_field.manager');
\Drupal::classResolver(ConfigEntityUpdater::class)->update($sandbox, 'entity_view_display', function (EntityViewDisplayInterface $display) use ($entity_field_manager) {
\Drupal::classResolver(ConfigEntityUpdater::class)->update($sandbox, 'entity_view_display', function (LayoutEntityDisplayInterface $display) use ($entity_field_manager) {
if (!$display->isLayoutBuilderEnabled()) {
return FALSE;
}
$extra_fields = $entity_field_manager->getExtraFields($display->getTargetEntityTypeId(), $display->getTargetBundle());
$components = $display->getComponents();
// Sort the components to avoid them being reordered by setComponent().
......
......@@ -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:
......
......@@ -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:
......
<?php
namespace Drupal\layout_builder\Access;
use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\layout_builder\SectionStorageInterface;
use Symfony\Component\Routing\Route;
/**
* Provides an access check for the Layout Builder defaults.
*
* @internal
*/
class LayoutBuilderAccessCheck implements AccessInterface {
/**
* Checks routing access to the layout.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
* @param \Drupal\Core\Session\AccountInterface $account
* The current user.
* @param \Symfony\Component\Routing\Route $route
* The route to check against.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
public function access(SectionStorageInterface $section_storage, AccountInterface $account, Route $route) {
$operation = $route->getRequirement('_layout_builder_access');
$access = $section_storage->access($operation, $account, TRUE);
if ($access instanceof RefinableCacheableDependencyInterface) {
$access->addCacheableDependency($section_storage);
}
return $access;
}
}
......@@ -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.
......
......@@ -58,6 +58,30 @@ public function setOverridable($overridable = TRUE) {
return $this;
}
/**
* {@inheritdoc}
*/
public function isLayoutBuilderEnabled() {
return (bool) $this->getThirdPartySetting('layout_builder', 'enabled');
}
/**
* {@inheritdoc}
*/
public function enableLayoutBuilder() {
$this->setThirdPartySetting('layout_builder', 'enabled', TRUE);
return $this;
}
/**
* {@inheritdoc}
*/
public function disableLayoutBuilder() {
$this->setOverridable(FALSE);
$this->setThirdPartySetting('layout_builder', 'enabled', FALSE);
return $this;
}
/**
* {@inheritdoc}
*/
......@@ -92,6 +116,27 @@ public function preSave(EntityStorageInterface $storage) {
$field->delete();
}
}
$already_enabled = isset($this->original) ? $this->original->isLayoutBuilderEnabled() : FALSE;
$set_enabled = $this->isLayoutBuilderEnabled();
if ($already_enabled !== $set_enabled) {
if ($set_enabled) {
// 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);
}
}
else {
// When being disabled, remove all existing section data.
while (count($this) > 0) {
$this->removeSection(0);
}
}
}
}
/**
......@@ -153,6 +198,9 @@ protected function contextRepository() {
*/
public function buildMultiple(array $entities) {
$build_list = parent::buildMultiple($entities);
if (!$this->isLayoutBuilderEnabled()) {
return $build_list;
}
/** @var \Drupal\Core\Entity\EntityInterface $entity */
foreach ($entities as $id => $entity) {
......@@ -272,6 +320,11 @@ public function setComponent($name, array $options = []) {
return $this;
}
// Only continue if Layout Builder is enabled.
if (!$this->isLayoutBuilderEnabled()) {
return $this;
}
// Retrieve the updated options after the parent:: call.
$options = $this->content[$name];
// Provide backwards compatibility by converting to a section component.
......
......@@ -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.
......
<?php
namespace Drupal\layout_builder\Form;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\layout_builder\DefaultsSectionStorageInterface;
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
use Drupal\layout_builder\SectionStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Disables Layout Builder for a given default.
*/
class LayoutBuilderDisableForm extends ConfirmFormBase {
/**
* The layout tempstore repository.
*
* @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
*/
protected $layoutTempstoreRepository;
/**
* The section storage.
*
* @var \Drupal\layout_builder\DefaultsSectionStorageInterface
*/
protected $sectionStorage;
/**
* Constructs a new RevertOverridesForm.
*
* @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
* The layout tempstore repository.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger service.
*/
public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository, MessengerInterface $messenger) {
$this->layoutTempstoreRepository = $layout_tempstore_repository;
$this->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());
}
}
......@@ -2,6 +2,7 @@
namespace Drupal\layout_builder\Form;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\field_ui\Form\EntityViewDisplayEditForm;
......@@ -28,7 +29,7 @@ class LayoutBuilderEntityViewDisplayForm extends EntityViewDisplayEditForm {
/**
* The storage section.
*
* @var \Drupal\layout_builder\SectionStorageInterface
* @var \Drupal\layout_builder\DefaultsSectionStorageInterface
*/
protected $sectionStorage;
......@@ -46,10 +47,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 +65,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']['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 +92,26 @@ public function form(array $form, FormStateInterface $form_state) {
'@entity' => $entity_type->getSingularLabel(),
]),
'#default_value' => $this->entity->isOverridable(),
'#states' => [
'disabled' => [
':input[name="layout[enabled]"]' => ['checked' => FALSE],
],
'invisible' => [
':input[name="layout[enabled]"]' => ['checked' => FALSE],
],
],
];
if (!$is_enabled) {
$form['layout']['allow_custom']['#attributes']['disabled'] = 'disabled';
}
// Prevent turning off overrides while any exist.
if ($this->hasOverrides($this->entity)) {
$form['layout']['enabled']['#disabled'] = TRUE;
$form['layout']['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['layout']['allow_custom']['#states']);
unset($form['#entity_builders']['layout_builder']);
}
}
return $form;
......@@ -112,26 +140,62 @@ protected function hasOverrides(LayoutEntityDisplayInterface $display) {
return (bool) $query->count()->execute();
}
/**
* {@inheritdoc}
*/
protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state) {
// Do not process field values if Layout Builder is or will be enabled.
$set_enabled = (bool) $form_state->getValue(['layout', 'enabled'], FALSE);
/** @var \Drupal\layout_builder\Entity\LayoutEntityDisplayInterface $entity */
$already_enabled = $entity->isLayoutBuilderEnabled();
if ($already_enabled || $set_enabled) {
$form['#fields'] = [];
$form['#extra'] = [];
}
parent::copyFormValuesToEntity($entity, $form, $form_state);
}
/**
* 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', 'enabled'], FALSE);
$already_enabled = $display->isLayoutBuilderEnabled();
if ($set_enabled) {
$overridable = (bool) $form_state->getValue(['layout', 'allow_custom'], FALSE);
$display->setOverridable($overridable);
if (!$already_enabled) {
$display->enableLayoutBuilder();
}
}
elseif ($already_enabled) {
$form_state->setRedirectUrl($this->sectionStorage->getLayoutBuilderUrl('disable'));
}
}
/**
* {@inheritdoc}
*/
protected function buildFieldRow(FieldDefinitionInterface $field_definition, array $form, FormStateInterface $form_state) {
// Intentionally empty.
if ($this->entity->isLayoutBuilderEnabled() || $field_definition->getType() === 'layout_section') {
return [];
}
return parent::buildFieldRow($field_definition, $form, $form_state);
}
/**
* {@inheritdoc}
*/
protected function buildExtraFieldRow($field_id, $extra_field) {
// Intentionally empty.
if ($this->entity->isLayoutBuilderEnabled()) {
return [];
}
return parent::buildExtraFieldRow($field_id, $extra_field);
}
}
<?php
namespace Drupal\layout_builder;
/**
* Provides methods for enabling and disabling Layout Builder.
*/
interface LayoutBuilderEnabledInterface {
/**
* Determines if Layout Builder is enabled.
*
* @return bool
* TRUE if Layout Builder is enabled, FALSE otherwise.
*/
public function isLayoutBuilderEnabled();
/**
* Enables the Layout Builder.
*
* @return $this
*/
public function enableLayoutBuilder();
/**
* Disables the Layout Builder.
*
* @return $this
*/
public function disableLayoutBuilder();
}
......@@ -3,12 +3,14 @@
namespace Drupal\layout_builder\Plugin\SectionStorage;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\Context\EntityContext;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Url;
use Drupal\field_ui\FieldUI;
use Drupal\layout_builder\DefaultsSectionStorageInterface;
......@@ -123,8 +125,8 @@ public function getRedirectUrl() {
/**
* {@inheritdoc}
*/
public function getLayoutBuilderUrl() {
return Url::fromRoute("layout_builder.{$this->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());
}
/**
......@@ -296,6 +298,29 @@ public function setThirdPartySetting($module, $key, $value) {
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}
*/
......@@ -325,4 +350,12 @@ public function getThirdPartyProviders() {
return $this->getDisplay()->getThirdPartyProviders();
}