Commit a9a53828 authored by larowlan's avatar larowlan

Issue #3004536 by tim.plunkett, tedbow, Sam152, phenaproxima, mark_fullmer:...

Issue #3004536 by tim.plunkett, tedbow, Sam152, phenaproxima, mark_fullmer: Move the Layout Builder UI into an entity form for better integration with other content authoring modules and core features
parent 1cc42e05
<?php
/**
* @file
* Hooks provided by the Layout Builder module.
*/
/**
* @addtogroup hooks
* @{
*/
/**
* Allows customization of the Layout Builder UI for per-entity overrides.
*
* The Layout Builder widget will be added with a weight of -10 after this hook
* is invoked.
*
* @see hook_entity_form_display_alter()
* @see \Drupal\layout_builder\Form\OverridesEntityForm::init()
*/
function hook_layout_builder_overrides_entity_form_display_alter(\Drupal\Core\Entity\Display\EntityFormDisplayInterface $display) {
$display->setComponent('moderation_state', [
'type' => 'moderation_state_default',
'weight' => 2,
'settings' => [],
]);
}
/**
* @} End of "addtogroup hooks".
*/
......@@ -6,6 +6,7 @@
*/
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Routing\RouteMatchInterface;
......@@ -13,7 +14,9 @@
use Drupal\field\FieldConfigInterface;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplayStorage;
use Drupal\layout_builder\Form\DefaultsEntityForm;
use Drupal\layout_builder\Form\LayoutBuilderEntityViewDisplayForm;
use Drupal\layout_builder\Form\OverridesEntityForm;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\layout_builder\Plugin\Block\ExtraFieldBlock;
use Drupal\layout_builder\InlineBlockEntityOperations;
......@@ -54,7 +57,15 @@ function layout_builder_entity_type_alter(array &$entity_types) {
$entity_types['entity_view_display']
->setClass(LayoutBuilderEntityViewDisplay::class)
->setStorageClass(LayoutBuilderEntityViewDisplayStorage::class)
->setFormClass('layout_builder', DefaultsEntityForm::class)
->setFormClass('edit', LayoutBuilderEntityViewDisplayForm::class);
// Ensure every fieldable entity type has a layout form.
foreach ($entity_types as $entity_type) {
if ($entity_type->entityClassImplements(FieldableEntityInterface::class)) {
$entity_type->setFormClass('layout_builder', OverridesEntityForm::class);
}
}
}
/**
......
......@@ -86,3 +86,10 @@ function layout_builder_post_update_cancel_link_to_discard_changes_form() {
function layout_builder_post_update_remove_layout_is_rebuilding() {
// Empty post-update hook.
}
/**
* Clear caches due to routing changes to move the Layout Builder UI to forms.
*/
function layout_builder_post_update_routing_entity_form() {
// Empty post-update hook.
}
......@@ -2,64 +2,17 @@
namespace Drupal\layout_builder\Controller;
use Drupal\Core\Ajax\AjaxHelperTrait;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\layout_builder\Context\LayoutBuilderContextTrait;
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
use Drupal\layout_builder\OverridesSectionStorageInterface;
use Drupal\layout_builder\SectionStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
/**
* Defines a controller to provide the Layout Builder admin UI.
*
* @internal
*/
class LayoutBuilderController implements ContainerInjectionInterface {
class LayoutBuilderController {
use LayoutBuilderContextTrait;
use StringTranslationTrait;
use AjaxHelperTrait;
/**
* The layout tempstore repository.
*
* @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
*/
protected $layoutTempstoreRepository;
/**
* The messenger service.
*
* @var \Drupal\Core\Messenger\MessengerInterface
*/
protected $messenger;
/**
* LayoutBuilderController constructor.
*
* @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
* The layout tempstore repository.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger service.
*/
public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository, MessengerInterface $messenger) {
$this->layoutTempstoreRepository = $layout_tempstore_repository;
$this->messenger = $messenger;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('layout_builder.tempstore_repository'),
$container->get('messenger')
);
}
/**
* Provides a title callback.
......@@ -90,27 +43,4 @@ public function layout(SectionStorageInterface $section_storage) {
];
}
/**
* Saves the layout.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse
* A redirect response.
*/
public function saveLayout(SectionStorageInterface $section_storage) {
$section_storage->save();
$this->layoutTempstoreRepository->delete($section_storage);
if ($section_storage instanceof OverridesSectionStorageInterface) {
$this->messenger->addMessage($this->t('The layout override has been saved.'));
}
else {
$this->messenger->addMessage($this->t('The layout has been saved.'));
}
return new RedirectResponse($section_storage->getRedirectUrl()->setAbsolute()->toString());
}
}
<?php
namespace Drupal\layout_builder\Form;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
use Drupal\layout_builder\SectionStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a form containing the Layout Builder UI for defaults.
*
* @internal
*/
class DefaultsEntityForm extends EntityForm {
/**
* Layout tempstore repository.
*
* @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
*/
protected $layoutTempstoreRepository;
/**
* The section storage.
*
* @var \Drupal\layout_builder\SectionStorageInterface
*/
protected $sectionStorage;
/**
* Constructs a new DefaultsEntityForm.
*
* @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
* The layout tempstore repository.
*/
public function __construct(LayoutTempstoreRepositoryInterface $layout_tempstore_repository) {
$this->layoutTempstoreRepository = $layout_tempstore_repository;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('layout_builder.tempstore_repository')
);
}
/**
* {@inheritdoc}
*/
public function getBaseFormId() {
return $this->getEntity()->getEntityTypeId() . '_layout_builder_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, SectionStorageInterface $section_storage = NULL) {
$form['layout_builder'] = [
'#type' => 'layout_builder',
'#section_storage' => $section_storage,
];
$this->sectionStorage = $section_storage;
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function buildEntity(array $form, FormStateInterface $form_state) {
// \Drupal\Core\Entity\EntityForm::buildEntity() clones the entity object.
// Keep it in sync with the one used by the section storage.
$this->setEntity($this->sectionStorage->getContextValue('display'));
$entity = parent::buildEntity($form, $form_state);
$this->sectionStorage->setContextValue('display', $entity);
return $entity;
}
/**
* {@inheritdoc}
*/
public function getEntityFromRouteMatch(RouteMatchInterface $route_match, $entity_type_id) {
$route_parameters = $route_match->getParameters()->all();
return $this->entityTypeManager->getStorage('entity_view_display')->load($route_parameters['entity_type_id'] . '.' . $route_parameters['bundle'] . '.' . $route_parameters['view_mode_name']);
}
/**
* {@inheritdoc}
*/
protected function actions(array $form, FormStateInterface $form_state) {
$actions = parent::actions($form, $form_state);
$actions['submit']['#value'] = $this->t('Save layout');
$actions['#weight'] = -1000;
$actions['discard_changes'] = [
'#type' => 'link',
'#title' => $this->t('Discard changes'),
'#attributes' => ['class' => ['button']],
'#url' => $this->sectionStorage->getLayoutBuilderUrl('discard_changes'),
];
return $actions;
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
$return = $this->sectionStorage->save();
$this->layoutTempstoreRepository->delete($this->sectionStorage);
$this->messenger()->addMessage($this->t('The layout has been saved.'));
$form_state->setRedirectUrl($this->sectionStorage->getRedirectUrl());
return $return;
}
/**
* Retrieves the section storage object.
*
* @return \Drupal\layout_builder\SectionStorageInterface
* The section storage for the current form.
*/
public function getSectionStorage() {
return $this->sectionStorage;
}
}
<?php
namespace Drupal\layout_builder\Form;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Entity\ContentEntityForm;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
use Drupal\layout_builder\SectionStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a form containing the Layout Builder UI for overrides.
*
* @internal
*/
class OverridesEntityForm extends ContentEntityForm {
/**
* Layout tempstore repository.
*
* @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface
*/
protected $layoutTempstoreRepository;
/**
* The section storage.
*
* @var \Drupal\layout_builder\SectionStorageInterface
*/
protected $sectionStorage;
/**
* Constructs a new OverridesEntityForm.
*
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
* The entity repository service.
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
* The entity type bundle service.
* @param \Drupal\Component\Datetime\TimeInterface $time
* The time service.
* @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository
* The layout tempstore repository.
*/
public function __construct(EntityRepositoryInterface $entity_repository, EntityTypeBundleInfoInterface $entity_type_bundle_info, TimeInterface $time, LayoutTempstoreRepositoryInterface $layout_tempstore_repository) {
parent::__construct($entity_repository, $entity_type_bundle_info, $time);
$this->layoutTempstoreRepository = $layout_tempstore_repository;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity.repository'),
$container->get('entity_type.bundle.info'),
$container->get('datetime.time'),
$container->get('layout_builder.tempstore_repository')
);
}
/**
* {@inheritdoc}
*/
public function getBaseFormId() {
return $this->getEntity()->getEntityTypeId() . '_layout_builder_form';
}
/**
* {@inheritdoc}
*/
protected function init(FormStateInterface $form_state) {
parent::init($form_state);
// Create a transient display that is not persisted, but used only for
// building the components required for the layout form.
$display = EntityFormDisplay::create([
'targetEntityType' => $this->getEntity()->getEntityTypeId(),
'bundle' => $this->getEntity()->bundle(),
]);
// Allow modules to choose if they are relevant to the layout form.
$this->moduleHandler->alter('layout_builder_overrides_entity_form_display', $display);
// Add the widget for Layout Builder after the alter.
$display->setComponent(OverridesSectionStorage::FIELD_NAME, [
'type' => 'layout_builder_widget',
'weight' => -10,
'settings' => [],
]);
$this->setFormDisplay($display, $form_state);
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, SectionStorageInterface $section_storage = NULL) {
$this->sectionStorage = $section_storage;
$form = parent::buildForm($form, $form_state);
// @todo \Drupal\layout_builder\Field\LayoutSectionItemList::defaultAccess()
// restricts all access to the field, explicitly allow access here until
// https://www.drupal.org/node/2942975 is resolved.
$form[OverridesSectionStorage::FIELD_NAME]['#access'] = TRUE;
return $form;
}
/**
* {@inheritdoc}
*/
public function buildEntity(array $form, FormStateInterface $form_state) {
// \Drupal\Core\Entity\EntityForm::buildEntity() clones the entity object.
// Keep it in sync with the one used by the section storage.
$this->setEntity($this->sectionStorage->getContextValue('entity'));
$entity = parent::buildEntity($form, $form_state);
$this->sectionStorage->setContextValue('entity', $entity);
return $entity;
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
$return = parent::save($form, $form_state);
$this->layoutTempstoreRepository->delete($this->sectionStorage);
$this->messenger()->addStatus($this->t('The layout override has been saved.'));
$form_state->setRedirectUrl($this->sectionStorage->getRedirectUrl());
return $return;
}
/**
* {@inheritdoc}
*/
protected function actions(array $form, FormStateInterface $form_state) {
$actions = parent::actions($form, $form_state);
$actions['submit']['#value'] = $this->t('Save layout');
$actions['delete']['#access'] = FALSE;
$actions['#weight'] = -1000;
$actions['discard_changes'] = [
'#type' => 'link',
'#title' => $this->t('Discard changes'),
'#attributes' => ['class' => ['button']],
'#url' => $this->sectionStorage->getLayoutBuilderUrl('discard_changes'),
];
// @todo This link should be conditionally displayed, see
// https://www.drupal.org/node/2917777.
$actions['revert'] = [
'#type' => 'link',
'#title' => $this->t('Revert to defaults'),
'#attributes' => ['class' => ['button']],
'#url' => $this->sectionStorage->getLayoutBuilderUrl('revert'),
];
return $actions;
}
/**
* Retrieves the section storage object.
*
* @return \Drupal\layout_builder\SectionStorageInterface
* The section storage for the current form.
*/
public function getSectionStorage() {
return $this->sectionStorage;
}
}
<?php
namespace Drupal\layout_builder\Plugin\Field\FieldWidget;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;
/**
* A widget to display the layout form.
*
* @FieldWidget(
* id = "layout_builder_widget",
* label = @Translation("Layout Builder Widget"),
* description = @Translation("A field widget for Layout Builder."),
* field_types = {
* "layout_section",
* },
* multiple_values = TRUE,
* )
*
* @internal
* Layout Builder is currently experimental and should only be leveraged by
* experimental modules and development releases of contributed modules.
* See https://www.drupal.org/core/experimental for more information.
*/
class LayoutBuilderWidget extends WidgetBase {
/**
* {@inheritdoc}
*/
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
$element += [
'#type' => 'layout_builder',
'#section_storage' => $this->getSectionStorage($form_state),
];
return $element;
}
/**
* {@inheritdoc}
*/
public function extractFormValues(FieldItemListInterface $items, array $form, FormStateInterface $form_state) {
// @todo This isn't resilient to being set twice, during validation and
// save https://www.drupal.org/project/drupal/issues/2833682.
if (!$form_state->isValidationComplete()) {
return;
}
$items->setValue($this->getSectionStorage($form_state)->getSections());
}
/**
* Gets the section storage.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return \Drupal\layout_builder\SectionStorageInterface
* The section storage loaded from the tempstore.
*/
private function getSectionStorage(FormStateInterface $form_state) {
return $form_state->getFormObject()->getSectionStorage();
}
}
......@@ -41,7 +41,7 @@
* experimental modules and development releases of contributed modules.
* See https://www.drupal.org/core/experimental for more information.
*/
class DefaultsSectionStorage extends SectionStorageBase implements ContainerFactoryPluginInterface, DefaultsSectionStorageInterface, SectionStorageLocalTaskProviderInterface {
class DefaultsSectionStorage extends SectionStorageBase implements ContainerFactoryPluginInterface, DefaultsSectionStorageInterface {
/**
* The entity type manager.
......@@ -172,7 +172,14 @@ public function buildRoutes(RouteCollection $collection) {
$options = $entity_route->getOptions();
$options['_admin_route'] = FALSE;
$this->buildLayoutRoutes($collection, $this->getPluginDefinition(), $path, $defaults, $requirements, $options, $entity_type_id);
$this->buildLayoutRoutes($collection, $this->getPluginDefinition(), $path, $defaults, $requirements, $options, $entity_type_id, 'entity_view_display');
// Set field_ui.route_enhancer to run on the manage layout form.
if (isset($defaults['bundle_key'])) {
$collection->get("layout_builder.defaults.$entity_type_id.view")
->setOption('_field_ui', TRUE)
->setDefault('bundle', '');
}
$route_names = [
"entity.entity_view_display.{$entity_type_id}.default",
......@@ -194,32 +201,6 @@ public function buildRoutes(RouteCollection $collection) {
}
}
/**
* {@inheritdoc}
*/
public function buildLocalTasks($base_plugin_definition) {
$local_tasks = [];
foreach ($this->getEntityTypes() as $entity_type_id => $entity_type) {
$local_tasks["layout_builder.defaults.$entity_type_id.view"] = $base_plugin_definition + [
'route_name' => "layout_builder.defaults.$entity_type_id.view",
'title' => $this->t('Manage layout'),
'base_route' => "layout_builder.defaults.$entity_type_id.view",
];
$local_tasks["layout_builder.defaults.$entity_type_id.save"] = $base_plugin_definition + [
'route_name' => "layout_builder.defaults.$entity_type_id.save",
'title' => $this->t('Save Layout'),
'parent_id' => "layout_builder_ui:layout_builder.defaults.$entity_type_id.view",
];
$local_tasks["layout_builder.defaults.$entity_type_id.discard_changes"] = $base_plugin_definition + [
'route_name' => "layout_builder.defaults.$entity_type_id.discard_changes",
'title' => $this->t('Discard changes'),
'weight' => 5,
'parent_id' => "layout_builder_ui:layout_builder.defaults.$entity_type_id.view",
];
}
return $local_tasks;
}
/**
* Returns an array of relevant entity types.
*
......@@ -228,7 +209,7 @@ public function buildLocalTasks($base_plugin_definition) {
*/
protected function getEntityTypes() {
return array_filter($this->entityTypeManager->getDefinitions(), function (EntityTypeInterface $entity_type) {
return $entity_type->entityClassImplements(FieldableEntityInterface::class) && $entity_type->hasViewBuilderClass() && $entity_type->get('field_ui_base_route');
return $entity_type->entityClassImplements(FieldableEntityInterface::class) && $entity_type->hasHandlerClass('form', 'layout_builder') && $entity_type->hasViewBuilderClass() && $entity_type->get('field_ui_base_route');
});
}
......
......@@ -220,7 +220,7 @@ public function buildRoutes(RouteCollection $collection) {
$options['parameters'][$entity_type_id]['type'] = 'entity:' . $entity_type_id;
$template = $entity_type->getLinkTemplate('canonical') . '/layout';
$this->buildLayoutRoutes($collection, $this->getPluginDefinition(), $template, $defaults, $requirements, $options, $entity_type_id);
$this->buildLayoutRoutes($collection, $this->getPluginDefinition(), $template, $defaults, $requirements, $options, $entity_type_id, $entity_type_id);
}
}
......@@ -237,28 +237,6 @@ public function buildLocalTasks($base_plugin_definition) {
'base_route' => "entity.$entity_type_id.canonical",
'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id],
];
$local_tasks["layout_builder.overrides.$entity_type_id.save"] = $base_plugin_definition + [
'route_name' => "layout_builder.overrides.$entity_type_id.save",
'title' => $this->t('Save Layout'),
'parent_id' => "layout_builder_ui:layout_builder.overrides.$entity_type_id.view",
'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id],
];
$local_tasks["layout_builder.overrides.$entity_type_id.discard_changes"] = $base_plugin_definition + [
'route_name' => "layout_builder.overrides.$entity_type_id.discard_changes",
'title' => $this->t('Discard changes'),
'parent_id' => "layout_builder_ui:layout_builder.overrides.$entity_type_id.view",
'weight' => 5,
'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id],
];
// @todo This link should be conditionally displayed, see
// https://www.drupal.org/node/2917777.
$local_tasks["layout_builder.overrides.$entity_type_id.revert"] = $base_plugin_definition + [
'route_name' => "layout_builder.overrides.$entity_type_id.revert",
'title' => $this->t('Revert to defaults'),
'parent_id' => "layout_builder_ui:layout_builder.overrides.$entity_type_id.view",
'weight' => 10,
'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id],
];
}
return $local_tasks;
}
......@@ -285,7 +263,7 @@ protected function hasIntegerId(EntityTypeInterface $entity_type) {
*/
protected function getEntityTypes() {
return array_filter($this->entityTypeManager->getDefinitions(), function (EntityTypeInterface $entity_type) {
return $entity_type->entityClassImplements(FieldableEntityInterface::class) && $entity_type->hasViewBuilderClass() && $entity_type->hasLinkTemplate('canonical');
return $entity_type->entityClassImplements(FieldableEntityInterface::class) && $entity_type->hasHandlerClass('form', 'layout_builder') && $entity_type->hasViewBuilderClass() && $entity_type->hasLinkTemplate('canonical');
});
}
......
......@@ -36,8 +36,10 @@ trait LayoutBuilderRoutesTrait {
* (optional) An array of options.
* @param string $route_name_prefix
* (optional) The prefix to use for the route name.
* @param string $entity_type_id
* (optional) The entity type ID, if available.
*/
protected function buildLayoutRoutes(RouteCollection $collection, SectionStorageDefinition $definition, $path, array $defaults = [], array $requirements = [], array $options = [], $route_name_prefix = '') {
protected function buildLayoutRoutes(RouteCollection $collection, SectionStorageDefinition $definition, $path, array $defaults = [], array $requirements = [], array $options = [], $route_name_prefix = '', $entity_type_id = '') {
$type = $definition->id();
$defaults['section_storage_type'] = $type;
// Provide an empty value to allow the section storage to be upcast.
......@@ -60,22 +62,20 @@ protected function buildLayoutRoutes(RouteCollection $collection, SectionStorage
}
$main_defaults = $defaults;
$main_defaults['_controller'] = '\Drupal\layout_builder\Controller\LayoutBuilderController::layout';
$main_options = $options;
if ($entity_type_id) {
$main_defaults['_entity_form'] = "$entity_type_id.layout_builder";
}
else {
$main_defaults['_controller'] = '\Drupal\layout_builder\Controller\LayoutBuilderController::layout';
}
$main_defaults['_title_callback'] = '\Drupal\layout_builder\Controller\LayoutBuilderController::title';
$route = (new Route($path))
->setDefaults($main_defaults)
->setRequirements($requirements)
->setOptions($options);
->setOptions($main_options);
$collection->add("$route_name_prefix.view", $route);
$save_defaults = $defaults;
$save_defaults['_controller'] = '\Drupal\layout_builder\Controller\LayoutBuilderController::saveLayout';
$route = (new Route("$path/save"))
->setDefaults($save_defaults)
->setRequirements($requirements)