Unverified Commit c8054435 authored by larowlan's avatar larowlan

Issue #2927349 by tim.plunkett: Decouple the Layout Builder UI from entities

parent bcb5afd4
layout_builder.choose_section:
path: '/layout_builder/choose/section/{entity_type_id}/{entity}/{delta}'
path: '/layout_builder/choose/section/{section_storage_type}/{section_storage}/{delta}'
defaults:
_controller: '\Drupal\layout_builder\Controller\ChooseSectionController::build'
requirements:
......@@ -7,12 +7,11 @@ layout_builder.choose_section:
options:
_admin_route: TRUE
parameters:
entity:
type: entity:{entity_type_id}
section_storage:
layout_builder_tempstore: TRUE
layout_builder.add_section:
path: '/layout_builder/add/section/{entity_type_id}/{entity}/{delta}/{plugin_id}'
path: '/layout_builder/add/section/{section_storage_type}/{section_storage}/{delta}/{plugin_id}'
defaults:
_controller: '\Drupal\layout_builder\Controller\AddSectionController::build'
requirements:
......@@ -20,12 +19,11 @@ layout_builder.add_section:
options:
_admin_route: TRUE
parameters:
entity:
type: entity:{entity_type_id}
section_storage:
layout_builder_tempstore: TRUE
layout_builder.configure_section:
path: '/layout_builder/configure/section/{entity_type_id}/{entity}/{delta}/{plugin_id}'
path: '/layout_builder/configure/section/{section_storage_type}/{section_storage}/{delta}/{plugin_id}'
defaults:
_title: 'Configure section'
_form: '\Drupal\layout_builder\Form\ConfigureSectionForm'
......@@ -37,12 +35,11 @@ layout_builder.configure_section:
options:
_admin_route: TRUE
parameters:
entity:
type: entity:{entity_type_id}
section_storage:
layout_builder_tempstore: TRUE
layout_builder.remove_section:
path: '/layout_builder/remove/section/{entity_type_id}/{entity}/{delta}'
path: '/layout_builder/remove/section/{section_storage_type}/{section_storage}/{delta}'
defaults:
_form: '\Drupal\layout_builder\Form\RemoveSectionForm'
requirements:
......@@ -50,12 +47,11 @@ layout_builder.remove_section:
options:
_admin_route: TRUE
parameters:
entity:
type: entity:{entity_type_id}
section_storage:
layout_builder_tempstore: TRUE
layout_builder.choose_block:
path: '/layout_builder/choose/block/{entity_type_id}/{entity}/{delta}/{region}'
path: '/layout_builder/choose/block/{section_storage_type}/{section_storage}/{delta}/{region}'
defaults:
_controller: '\Drupal\layout_builder\Controller\ChooseBlockController::build'
requirements:
......@@ -63,12 +59,11 @@ layout_builder.choose_block:
options:
_admin_route: TRUE
parameters:
entity:
type: entity:{entity_type_id}
section_storage:
layout_builder_tempstore: TRUE
layout_builder.add_block:
path: '/layout_builder/add/block/{entity_type_id}/{entity}/{delta}/{region}/{plugin_id}'
path: '/layout_builder/add/block/{section_storage_type}/{section_storage}/{delta}/{region}/{plugin_id}'
defaults:
_form: '\Drupal\layout_builder\Form\AddBlockForm'
requirements:
......@@ -76,12 +71,11 @@ layout_builder.add_block:
options:
_admin_route: TRUE
parameters:
entity:
type: entity:{entity_type_id}
section_storage:
layout_builder_tempstore: TRUE
layout_builder.update_block:
path: '/layout_builder/update/block/{entity_type_id}/{entity}/{delta}/{region}/{uuid}'
path: '/layout_builder/update/block/{section_storage_type}/{section_storage}/{delta}/{region}/{uuid}'
defaults:
_form: '\Drupal\layout_builder\Form\UpdateBlockForm'
requirements:
......@@ -89,12 +83,11 @@ layout_builder.update_block:
options:
_admin_route: TRUE
parameters:
entity:
type: entity:{entity_type_id}
section_storage:
layout_builder_tempstore: TRUE
layout_builder.remove_block:
path: '/layout_builder/remove/block/{entity_type_id}/{entity}/{delta}/{region}/{uuid}'
path: '/layout_builder/remove/block/{section_storage_type}/{section_storage}/{delta}/{region}/{uuid}'
defaults:
_form: '\Drupal\layout_builder\Form\RemoveBlockForm'
requirements:
......@@ -102,12 +95,11 @@ layout_builder.remove_block:
options:
_admin_route: TRUE
parameters:
entity:
type: entity:{entity_type_id}
section_storage:
layout_builder_tempstore: TRUE
layout_builder.move_block:
path: '/layout_builder/move/block/{entity_type_id}/{entity}/{delta_from}/{delta_to}/{region_from}/{region_to}/{block_uuid}/{preceding_block_uuid}'
path: '/layout_builder/move/block/{section_storage_type}/{section_storage}/{delta_from}/{delta_to}/{region_from}/{region_to}/{block_uuid}/{preceding_block_uuid}'
defaults:
_controller: '\Drupal\layout_builder\Controller\MoveBlockController::build'
delta_from: null
......@@ -121,8 +113,7 @@ layout_builder.move_block:
options:
_admin_route: TRUE
parameters:
entity:
type: entity:{entity_type_id}
section_storage:
layout_builder_tempstore: TRUE
route_callbacks:
......
services:
layout_builder.tempstore_repository:
class: Drupal\layout_builder\LayoutTempstoreRepository
arguments: ['@user.shared_tempstore', '@entity_type.manager']
arguments: ['@user.shared_tempstore']
access_check.entity.layout:
class: Drupal\layout_builder\Access\LayoutSectionAccessCheck
tags:
......@@ -15,9 +15,12 @@ services:
- { name: route_enhancer }
layout_builder.param_converter:
class: Drupal\layout_builder\Routing\LayoutTempstoreParamConverter
arguments: ['@entity.manager', '@layout_builder.tempstore_repository']
arguments: ['@layout_builder.tempstore_repository', '@class_resolver']
tags:
- { name: paramconverter, priority: 10 }
layout_builder.section_storage_param_converter.overrides:
class: Drupal\layout_builder\Routing\SectionStorageOverridesParamConverter
arguments: ['@entity.manager']
cache_context.layout_builder_is_active:
class: Drupal\layout_builder\Cache\LayoutBuilderIsActiveCacheContext
arguments: ['@current_route_match']
......
......@@ -3,10 +3,10 @@
namespace Drupal\layout_builder\Access;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\layout_builder\SectionStorageInterface;
/**
* Provides an access check for the Layout Builder UI.
......@@ -16,7 +16,7 @@
class LayoutSectionAccessCheck implements AccessInterface {
/**
* Checks routing access to layout for the entity.
* Checks routing access to the layout.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The current route match.
......@@ -27,24 +27,20 @@ class LayoutSectionAccessCheck implements AccessInterface {
* The access result.
*/
public function access(RouteMatchInterface $route_match, AccountInterface $account) {
// Attempt to retrieve the generic 'entity' parameter, otherwise look up the
// specific entity via the entity type ID.
$entity = $route_match->getParameter('entity') ?: $route_match->getParameter($route_match->getParameter('entity_type_id'));
$section_storage = $route_match->getParameter('section_storage');
// If we don't have an entity, forbid access.
if (empty($entity)) {
if (empty($section_storage)) {
return AccessResult::forbidden()->addCacheContexts(['route']);
}
// If the entity isn't fieldable, forbid access.
if (!$entity instanceof FieldableEntityInterface || !$entity->hasField('layout_builder__layout')) {
if (!$section_storage instanceof SectionStorageInterface) {
$access = AccessResult::forbidden();
}
else {
$access = AccessResult::allowedIfHasPermission($account, 'configure any layout');
}
return $access->addCacheableDependency($entity);
return $access->addCacheableDependency($section_storage);
}
}
......@@ -4,9 +4,9 @@
use Drupal\Core\DependencyInjection\ClassResolverInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
use Drupal\layout_builder\Section;
use Drupal\layout_builder\SectionStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
......@@ -51,10 +51,10 @@ public static function create(ContainerInterface $container) {
}
/**
* Add the layout to the entity field in a tempstore.
* Adds the new section.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity.
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
* @param int $delta
* The delta of the section to splice.
* @param string $plugin_id
......@@ -63,18 +63,16 @@ public static function create(ContainerInterface $container) {
* @return \Symfony\Component\HttpFoundation\Response
* The controller response.
*/
public function build(EntityInterface $entity, $delta, $plugin_id) {
/** @var \Drupal\layout_builder\SectionStorageInterface $field_list */
$field_list = $entity->layout_builder__layout;
$field_list->insertSection($delta, new Section($plugin_id));
public function build(SectionStorageInterface $section_storage, $delta, $plugin_id) {
$section_storage->insertSection($delta, new Section($plugin_id));
$this->layoutTempstoreRepository->set($entity);
$this->layoutTempstoreRepository->set($section_storage);
if ($this->isAjax()) {
return $this->rebuildAndClose($entity);
return $this->rebuildAndClose($section_storage);
}
else {
$url = $entity->toUrl('layout-builder');
$url = $section_storage->getLayoutBuilderUrl();
return new RedirectResponse($url->setAbsolute()->toString());
}
}
......
......@@ -4,8 +4,8 @@
use Drupal\Core\Block\BlockManagerInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Url;
use Drupal\layout_builder\SectionStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
......@@ -46,8 +46,8 @@ public static function create(ContainerInterface $container) {
/**
* Provides the UI for choosing a new block.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity.
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
* @param int $delta
* The delta of the section to splice.
* @param string $region
......@@ -56,7 +56,7 @@ public static function create(ContainerInterface $container) {
* @return array
* A render array.
*/
public function build(EntityInterface $entity, $delta, $region) {
public function build(SectionStorageInterface $section_storage, $delta, $region) {
$build['#type'] = 'container';
$build['#attributes']['class'][] = 'block-categories';
......@@ -72,8 +72,8 @@ public function build(EntityInterface $entity, $delta, $region) {
'title' => $block['admin_label'],
'url' => Url::fromRoute('layout_builder.add_block',
[
'entity_type_id' => $entity->getEntityTypeId(),
'entity' => $entity->id(),
'section_storage_type' => $section_storage->getStorageType(),
'section_storage' => $section_storage->getStorageId(),
'delta' => $delta,
'region' => $region,
'plugin_id' => $block_id,
......
......@@ -3,11 +3,11 @@
namespace Drupal\layout_builder\Controller;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Layout\LayoutPluginManagerInterface;
use Drupal\Core\Plugin\PluginFormInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Drupal\layout_builder\SectionStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
......@@ -49,15 +49,15 @@ public static function create(ContainerInterface $container) {
/**
* Choose a layout plugin to add as a section.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity.
* @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(EntityInterface $entity, $delta) {
public function build(SectionStorageInterface $section_storage, $delta) {
$output['#title'] = $this->t('Choose a layout');
$items = [];
......@@ -75,8 +75,8 @@ public function build(EntityInterface $entity, $delta) {
'#url' => Url::fromRoute(
$layout instanceof PluginFormInterface ? 'layout_builder.configure_section' : 'layout_builder.add_section',
[
'entity_type_id' => $entity->getEntityTypeId(),
'entity' => $entity->id(),
'section_storage_type' => $section_storage->getStorageType(),
'section_storage' => $section_storage->getStorageId(),
'delta' => $delta,
'plugin_id' => $plugin_id,
]
......
......@@ -3,12 +3,12 @@
namespace Drupal\layout_builder\Controller;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Plugin\PluginFormInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Drupal\layout_builder\LayoutTempstoreRepositoryInterface;
use Drupal\layout_builder\Section;
use Drupal\layout_builder\SectionStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
......@@ -50,48 +50,38 @@ public static function create(ContainerInterface $container) {
/**
* Provides a title callback.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity.
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
*
* @return string
* The title for the layout page.
*/
public function title(EntityInterface $entity) {
return $this->t('Edit layout for %label', ['%label' => $entity->label()]);
public function title(SectionStorageInterface $section_storage) {
return $this->t('Edit layout for %label', ['%label' => $section_storage->label()]);
}
/**
* Renders the Layout UI.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity.
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
* @param bool $is_rebuilding
* (optional) Indicates if the layout is rebuilding, defaults to FALSE.
*
* @return array
* A render array.
*/
public function layout(EntityInterface $entity, $is_rebuilding = FALSE) {
$entity_id = $entity->id();
$entity_type_id = $entity->getEntityTypeId();
/** @var \Drupal\layout_builder\SectionStorageInterface $field_list */
$field_list = $entity->layout_builder__layout;
// For a new layout override, begin with a single section of one column.
if (!$is_rebuilding && $field_list->count() === 0) {
$field_list->appendSection(new Section('layout_onecol'));
$this->layoutTempstoreRepository->set($entity);
}
public function layout(SectionStorageInterface $section_storage, $is_rebuilding = FALSE) {
$this->prepareLayout($section_storage, $is_rebuilding);
$output = [];
$count = 0;
foreach ($field_list->getSections() as $section) {
$output[] = $this->buildAddSectionLink($entity_type_id, $entity_id, $count);
$output[] = $this->buildAdministrativeSection($section, $entity, $count);
for ($i = 0; $i < $section_storage->count(); $i++) {
$output[] = $this->buildAddSectionLink($section_storage, $count);
$output[] = $this->buildAdministrativeSection($section_storage, $count);
$count++;
}
$output[] = $this->buildAddSectionLink($entity_type_id, $entity_id, $count);
$output[] = $this->buildAddSectionLink($section_storage, $count);
$output['#attached']['library'][] = 'layout_builder/drupal.layout_builder';
$output['#type'] = 'container';
$output['#attributes']['id'] = 'layout-builder';
......@@ -100,28 +90,51 @@ public function layout(EntityInterface $entity, $is_rebuilding = FALSE) {
return $output;
}
/**
* Prepares a layout for use in the UI.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
* @param bool $is_rebuilding
* Indicates if the layout is rebuilding.
*/
protected function prepareLayout(SectionStorageInterface $section_storage, $is_rebuilding) {
// For a new layout, begin with a single section of one column.
if (!$is_rebuilding && $section_storage->count() === 0) {
$sections = [];
if (!$sections) {
$sections[] = new Section('layout_onecol');
}
foreach ($sections as $section) {
$section_storage->appendSection($section);
}
$this->layoutTempstoreRepository->set($section_storage);
}
}
/**
* Builds a link to add a new section at a given delta.
*
* @param string $entity_type_id
* The entity type.
* @param string $entity_id
* The entity ID.
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
* @param int $delta
* The delta of the section to splice.
*
* @return array
* A render array for a link.
*/
protected function buildAddSectionLink($entity_type_id, $entity_id, $delta) {
protected function buildAddSectionLink(SectionStorageInterface $section_storage, $delta) {
$storage_type = $section_storage->getStorageType();
$storage_id = $section_storage->getStorageId();
return [
'link' => [
'#type' => 'link',
'#title' => $this->t('Add Section'),
'#url' => Url::fromRoute('layout_builder.choose_section',
[
'entity_type_id' => $entity_type_id,
'entity' => $entity_id,
'section_storage_type' => $storage_type,
'section_storage' => $storage_id,
'delta' => $delta,
],
[
......@@ -143,19 +156,18 @@ protected function buildAddSectionLink($entity_type_id, $entity_id, $delta) {
/**
* Builds the render array for the layout section while editing.
*
* @param \Drupal\layout_builder\Section $section
* The layout section.
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity.
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
* @param int $delta
* The delta of the section.
*
* @return array
* The render array for a given section.
*/
protected function buildAdministrativeSection(Section $section, EntityInterface $entity, $delta) {
$entity_type_id = $entity->getEntityTypeId();
$entity_id = $entity->id();
protected function buildAdministrativeSection(SectionStorageInterface $section_storage, $delta) {
$storage_type = $section_storage->getStorageType();
$storage_id = $section_storage->getStorageId();
$section = $section_storage->getSection($delta);
$layout = $section->getLayout();
$build = $section->toRenderArray();
......@@ -169,8 +181,8 @@ protected function buildAdministrativeSection(Section $section, EntityInterface
$build[$region][$uuid]['#contextual_links'] = [
'layout_builder_block' => [
'route_parameters' => [
'entity_type_id' => $entity_type_id,
'entity' => $entity_id,
'section_storage_type' => $storage_type,
'section_storage' => $storage_id,
'delta' => $delta,
'region' => $region,
'uuid' => $uuid,
......@@ -185,8 +197,8 @@ protected function buildAdministrativeSection(Section $section, EntityInterface
'#title' => $this->t('Add Block'),
'#url' => Url::fromRoute('layout_builder.choose_block',
[
'entity_type_id' => $entity_type_id,
'entity' => $entity_id,
'section_storage_type' => $storage_type,
'section_storage' => $storage_id,
'delta' => $delta,
'region' => $region,
],
......@@ -207,8 +219,8 @@ protected function buildAdministrativeSection(Section $section, EntityInterface
}
$build['#attributes']['data-layout-update-url'] = Url::fromRoute('layout_builder.move_block', [
'entity_type_id' => $entity_type_id,
'entity' => $entity_id,
'section_storage_type' => $storage_type,
'section_storage' => $storage_id,
])->toString();
$build['#attributes']['data-layout-delta'] = $delta;
$build['#attributes']['class'][] = 'layout-builder--layout';
......@@ -223,8 +235,8 @@ protected function buildAdministrativeSection(Section $section, EntityInterface
'#title' => $this->t('Configure section'),
'#access' => $layout instanceof PluginFormInterface,
'#url' => Url::fromRoute('layout_builder.configure_section', [
'entity_type_id' => $entity_type_id,
'entity' => $entity_id,
'section_storage_type' => $storage_type,
'section_storage' => $storage_id,
'delta' => $delta,
]),
'#attributes' => [
......@@ -237,8 +249,8 @@ protected function buildAdministrativeSection(Section $section, EntityInterface
'#type' => 'link',
'#title' => $this->t('Remove section'),
'#url' => Url::fromRoute('layout_builder.remove_section', [
'entity_type_id' => $entity_type_id,
'entity' => $entity_id,
'section_storage_type' => $storage_type,
'section_storage' => $storage_id,
'delta' => $delta,
]),
'#attributes' => [
......@@ -254,30 +266,30 @@ protected function buildAdministrativeSection(Section $section, EntityInterface
/**
* Saves the layout.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity.
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse
* A redirect response.
*/
public function saveLayout(EntityInterface $entity) {
$entity->save();
$this->layoutTempstoreRepository->delete($entity);
return new RedirectResponse($entity->toUrl()->setAbsolute()->toString());
public function saveLayout(SectionStorageInterface $section_storage) {
$section_storage->save();
$this->layoutTempstoreRepository->delete($section_storage);
return new RedirectResponse($section_storage->getCanonicalUrl()->setAbsolute()->toString());
}
/**
* Cancels the layout.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity.
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse
* A redirect response.
*/
public function cancelLayout(EntityInterface $entity) {
$this->layoutTempstoreRepository->delete($entity);
return new RedirectResponse($entity->toUrl()->setAbsolute()->toString());
public function cancelLayout(SectionStorageInterface $section_storage) {
$this->layoutTempstoreRepository->delete($section_storage);
return new RedirectResponse($section_storage->getCanonicalUrl()->setAbsolute()->toString());
}
}
......@@ -5,7 +5,7 @@
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\CloseDialogCommand;
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\Core\Entity\EntityInterface;
use Drupal\layout_builder\SectionStorageInterface;
/**
* Provides AJAX responses to rebuild the Layout Builder.
......@@ -24,15 +24,15 @@ trait LayoutRebuildTrait {
/**
* Rebuilds the layout.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity.
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* An AJAX response to either rebuild the layout and close the dialog, or
* reload the page.
*/