composer update

This commit is contained in:
Oliver Davies 2019-01-24 08:00:03 +00:00
parent f6abc3dce2
commit 71dfaca858
1753 changed files with 45274 additions and 14619 deletions

View file

@ -85,3 +85,32 @@
display: block;
padding-top: 0.55em;
}
#drupal-off-canvas .inline-block-create-button {
display: block;
padding: 24px;
padding-left: 44px;
font-size: 16px;
color: #eee;
background: url(../../../misc/icons/bebebe/plus.svg) transparent 16px no-repeat;
}
#drupal-off-canvas .inline-block-create-button,
#drupal-off-canvas .inline-block-list__item {
margin: 0 -20px;
background-color: #444;
}
#drupal-off-canvas .inline-block-create-button:hover,
#drupal-off-canvas .inline-block-list__item:hover {
background-color: #333;
}
#drupal-off-canvas .inline-block-list {
margin-bottom: 15px;
}
#drupal-off-canvas .inline-block-list__item {
display: block;
padding: 15px 0 15px 25px;
}

View file

@ -9,3 +9,5 @@ dependencies:
- drupal:contextual
# @todo Discuss removing in https://www.drupal.org/project/drupal/issues/2935999.
- drupal:field_ui
# @todo Discuss removing in https://www.drupal.org/project/drupal/issues/3003610.
- drupal:block

View file

@ -19,6 +19,7 @@ use Drupal\layout_builder\Plugin\Block\ExtraFieldBlock;
use Drupal\layout_builder\InlineBlockEntityOperations;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
/**
* Implements hook_help().
@ -62,8 +63,8 @@ function layout_builder_entity_type_alter(array &$entity_types) {
function layout_builder_form_entity_form_display_edit_form_alter(&$form, FormStateInterface $form_state) {
// Hides the Layout Builder field. It is rendered directly in
// \Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay::buildMultiple().
unset($form['fields']['layout_builder__layout']);
$key = array_search('layout_builder__layout', $form['#fields']);
unset($form['fields'][OverridesSectionStorage::FIELD_NAME]);
$key = array_search(OverridesSectionStorage::FIELD_NAME, $form['#fields']);
if ($key !== FALSE) {
unset($form['#fields'][$key]);
}
@ -177,7 +178,7 @@ function layout_builder_cron() {
function layout_builder_plugin_filter_block_alter(array &$definitions, array $extra, $consumer) {
// @todo Determine the 'inline_block' blocks should be allowed outside
// of layout_builder https://www.drupal.org/node/2979142.
if ($consumer !== 'layout_builder') {
if ($consumer !== 'layout_builder' || !isset($extra['list']) || $extra['list'] !== 'inline_blocks') {
foreach ($definitions as $id => $definition) {
if ($definition['id'] === 'inline_block') {
unset($definitions[$id]);
@ -202,3 +203,21 @@ function layout_builder_block_content_access(EntityInterface $entity, $operation
}
return AccessResult::forbidden();
}
/**
* Implements hook_plugin_filter_TYPE__CONSUMER_alter().
*/
function layout_builder_plugin_filter_block__block_ui_alter(array &$definitions, array $extra) {
foreach ($definitions as $id => $definition) {
// Filter out any layout_builder definition with required contexts.
if ($definition['provider'] === 'layout_builder' && !empty($definition['context'])) {
/** @var \Drupal\Core\Plugin\Context\ContextDefinitionInterface $context */
foreach ($definition['context'] as $context) {
if ($context->isRequired()) {
unset($definitions[$id]);
break;
}
}
}
}
}

View file

@ -80,6 +80,19 @@ layout_builder.add_block:
section_storage:
layout_builder_tempstore: TRUE
layout_builder.choose_inline_block:
path: '/layout_builder/choose/inline-block/{section_storage_type}/{section_storage}/{delta}/{region}'
defaults:
_controller: '\Drupal\layout_builder\Controller\ChooseBlockController::inlineBlockList'
_title: 'Add a new Inline Block'
requirements:
_permission: 'configure any layout'
options:
_admin_route: TRUE
parameters:
section_storage:
layout_builder_tempstore: TRUE
layout_builder.update_block:
path: '/layout_builder/update/block/{section_storage_type}/{section_storage}/{delta}/{region}/{uuid}'
defaults:

View file

@ -5,6 +5,7 @@ namespace Drupal\layout_builder\Controller;
use Drupal\Core\Ajax\AjaxHelperTrait;
use Drupal\Core\Block\BlockManagerInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Drupal\layout_builder\Context\LayoutBuilderContextTrait;
@ -29,14 +30,24 @@ class ChooseBlockController implements ContainerInjectionInterface {
*/
protected $blockManager;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* ChooseBlockController constructor.
*
* @param \Drupal\Core\Block\BlockManagerInterface $block_manager
* The block manager.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(BlockManagerInterface $block_manager) {
public function __construct(BlockManagerInterface $block_manager, EntityTypeManagerInterface $entity_type_manager) {
$this->blockManager = $block_manager;
$this->entityTypeManager = $entity_type_manager;
}
/**
@ -44,7 +55,8 @@ class ChooseBlockController implements ContainerInjectionInterface {
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('plugin.manager.block')
$container->get('plugin.manager.block'),
$container->get('entity_type.manager')
);
}
@ -63,8 +75,43 @@ class ChooseBlockController implements ContainerInjectionInterface {
*/
public function build(SectionStorageInterface $section_storage, $delta, $region) {
$build['#title'] = $this->t('Choose a block');
$build['#type'] = 'container';
$build['#attributes']['class'][] = 'block-categories';
if ($this->entityTypeManager->hasDefinition('block_content_type') && $types = $this->entityTypeManager->getStorage('block_content_type')->loadMultiple()) {
if (count($types) === 1) {
$type = reset($types);
$plugin_id = 'inline_block:' . $type->id();
if ($this->blockManager->hasDefinition($plugin_id)) {
$url = Url::fromRoute('layout_builder.add_block', [
'section_storage_type' => $section_storage->getStorageType(),
'section_storage' => $section_storage->getStorageId(),
'delta' => $delta,
'region' => $region,
'plugin_id' => $plugin_id,
]);
}
}
else {
$url = Url::fromRoute('layout_builder.choose_inline_block', [
'section_storage_type' => $section_storage->getStorageType(),
'section_storage' => $section_storage->getStorageId(),
'delta' => $delta,
'region' => $region,
]);
}
if (isset($url)) {
$build['add_block'] = [
'#type' => 'link',
'#url' => $url,
'#title' => $this->t('Create @entity_type', [
'@entity_type' => $this->entityTypeManager->getDefinition('block_content')->getSingularLabel(),
]),
'#attributes' => $this->getAjaxAttributes(),
];
$build['add_block']['#attributes']['class'][] = 'inline-block-create-button';
}
}
$block_categories['#type'] = 'container';
$block_categories['#attributes']['class'][] = 'block-categories';
// @todo Explicitly cast delta to an integer, remove this in
// https://www.drupal.org/project/drupal/issues/2984509.
@ -75,35 +122,116 @@ class ChooseBlockController implements ContainerInjectionInterface {
'delta' => $delta,
'region' => $region,
]);
foreach ($this->blockManager->getGroupedDefinitions($definitions) as $category => $blocks) {
$build[$category]['#type'] = 'details';
$build[$category]['#open'] = TRUE;
$build[$category]['#title'] = $category;
$build[$category]['links'] = [
'#theme' => 'links',
];
foreach ($blocks as $block_id => $block) {
$link = [
'title' => $block['admin_label'],
'url' => Url::fromRoute('layout_builder.add_block',
[
'section_storage_type' => $section_storage->getStorageType(),
'section_storage' => $section_storage->getStorageId(),
'delta' => $delta,
'region' => $region,
'plugin_id' => $block_id,
]
),
];
if ($this->isAjax()) {
$link['attributes']['class'][] = 'use-ajax';
$link['attributes']['data-dialog-type'][] = 'dialog';
$link['attributes']['data-dialog-renderer'][] = 'off_canvas';
}
$build[$category]['links']['#links'][] = $link;
$grouped_definitions = $this->blockManager->getGroupedDefinitions($definitions);
foreach ($grouped_definitions as $category => $blocks) {
$block_categories[$category]['#type'] = 'details';
$block_categories[$category]['#open'] = TRUE;
$block_categories[$category]['#title'] = $category;
$block_categories[$category]['links'] = $this->getBlockLinks($section_storage, $delta, $region, $blocks);
}
$build['block_categories'] = $block_categories;
return $build;
}
/**
* Provides the UI for choosing a new inline block.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
* @param int $delta
* The delta of the section to splice.
* @param string $region
* The region the block is going in.
*
* @return array
* A render array.
*/
public function inlineBlockList(SectionStorageInterface $section_storage, $delta, $region) {
$definitions = $this->blockManager->getFilteredDefinitions('layout_builder', $this->getAvailableContexts($section_storage), [
'section_storage' => $section_storage,
'region' => $region,
'list' => 'inline_blocks',
]);
$blocks = $this->blockManager->getGroupedDefinitions($definitions);
$build = [];
if (isset($blocks['Inline blocks'])) {
$build['links'] = $this->getBlockLinks($section_storage, $delta, $region, $blocks['Inline blocks']);
$build['links']['#attributes']['class'][] = 'inline-block-list';
foreach ($build['links']['#links'] as &$link) {
$link['attributes']['class'][] = 'inline-block-list__item';
}
$build['back_button'] = [
'#type' => 'link',
'#url' => Url::fromRoute('layout_builder.choose_block',
[
'section_storage_type' => $section_storage->getStorageType(),
'section_storage' => $section_storage->getStorageId(),
'delta' => $delta,
'region' => $region,
]
),
'#title' => $this->t('Back'),
'#attributes' => $this->getAjaxAttributes(),
];
}
return $build;
}
/**
* Gets a render array of block links.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage.
* @param int $delta
* The delta of the section to splice.
* @param string $region
* The region the block is going in.
* @param array $blocks
* The information for each block.
*
* @return array
* The block links render array.
*/
protected function getBlockLinks(SectionStorageInterface $section_storage, $delta, $region, array $blocks) {
$links = [];
foreach ($blocks as $block_id => $block) {
$link = [
'title' => $block['admin_label'],
'url' => Url::fromRoute('layout_builder.add_block',
[
'section_storage_type' => $section_storage->getStorageType(),
'section_storage' => $section_storage->getStorageId(),
'delta' => $delta,
'region' => $region,
'plugin_id' => $block_id,
]
),
'attributes' => $this->getAjaxAttributes(),
];
$links[] = $link;
}
return [
'#theme' => 'links',
'#links' => $links,
];
}
/**
* Get dialog attributes if an ajax request.
*
* @return array
* The attributes array.
*/
protected function getAjaxAttributes() {
if ($this->isAjax()) {
return [
'class' => ['use-ajax'],
'data-dialog-type' => 'dialog',
'data-dialog-renderer' => 'off_canvas',
];
}
return [];
}
}

View file

@ -2,6 +2,7 @@
namespace Drupal\layout_builder\Controller;
use Drupal\Core\Ajax\AjaxHelperTrait;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Plugin\PluginFormInterface;
@ -24,6 +25,7 @@ class LayoutBuilderController implements ContainerInjectionInterface {
use LayoutBuilderContextTrait;
use StringTranslationTrait;
use AjaxHelperTrait;
/**
* The layout tempstore repository.
@ -90,6 +92,11 @@ class LayoutBuilderController implements ContainerInjectionInterface {
$this->prepareLayout($section_storage, $is_rebuilding);
$output = [];
if ($this->isAjax()) {
$output['status_messages'] = [
'#type' => 'status_messages',
];
}
$count = 0;
for ($i = 0; $i < $section_storage->count(); $i++) {
$output[] = $this->buildAddSectionLink($section_storage, $count);
@ -114,6 +121,11 @@ class LayoutBuilderController implements ContainerInjectionInterface {
* Indicates if the layout is rebuilding.
*/
protected function prepareLayout(SectionStorageInterface $section_storage, $is_rebuilding) {
// If the layout has pending changes, add a warning.
if ($this->layoutTempstoreRepository->has($section_storage)) {
$this->messenger->addWarning($this->t('You have unsaved changes.'));
}
// Only add sections if the layout is new and empty.
if (!$is_rebuilding && $section_storage->count() === 0) {
$sections = [];
@ -269,7 +281,7 @@ class LayoutBuilderController implements ContainerInjectionInterface {
],
'remove' => [
'#type' => 'link',
'#title' => $this->t('Remove section'),
'#title' => $this->t('Remove section <span class="visually-hidden">@section</span>', ['@section' => $delta + 1]),
'#url' => Url::fromRoute('layout_builder.remove_section', [
'section_storage_type' => $storage_type,
'section_storage' => $storage_id,

View file

@ -9,6 +9,7 @@ use Drupal\Core\Plugin\Context\EntityContext;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
use Drupal\layout_builder\Section;
use Drupal\layout_builder\SectionComponent;
use Drupal\layout_builder\SectionStorage\SectionStorageTrait;
@ -110,10 +111,10 @@ class LayoutBuilderEntityViewDisplay extends BaseEntityViewDisplay implements La
$bundle = $this->getTargetBundle();
if ($new_value) {
$this->addSectionField($entity_type_id, $bundle, 'layout_builder__layout');
$this->addSectionField($entity_type_id, $bundle, OverridesSectionStorage::FIELD_NAME);
}
else {
$this->removeSectionField($entity_type_id, $bundle, 'layout_builder__layout');
$this->removeSectionField($entity_type_id, $bundle, OverridesSectionStorage::FIELD_NAME);
}
}
@ -274,8 +275,8 @@ class LayoutBuilderEntityViewDisplay extends BaseEntityViewDisplay implements La
* The sections.
*/
protected function getRuntimeSections(FieldableEntityInterface $entity) {
if ($this->isOverridable() && !$entity->get('layout_builder__layout')->isEmpty()) {
return $entity->get('layout_builder__layout')->getSections();
if ($this->isOverridable() && !$entity->get(OverridesSectionStorage::FIELD_NAME)->isEmpty()) {
return $entity->get(OverridesSectionStorage::FIELD_NAME)->getSections();
}
return $this->getSections();

View file

@ -5,6 +5,8 @@ namespace Drupal\layout_builder\EventSubscriber;
use Drupal\block_content\Access\RefinableDependentAccessInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Block\BlockPluginInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\PreviewFallbackInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\layout_builder\Access\LayoutPreviewAccessAllowed;
use Drupal\layout_builder\Event\SectionComponentBuildRenderArrayEvent;
@ -88,6 +90,14 @@ class BlockComponentRenderArray implements EventSubscriberInterface {
if ($access->isAllowed()) {
$event->addCacheableDependency($block);
$content = $block->build();
$is_content_empty = Element::isEmpty($content);
$is_placeholder_ready = $event->inPreview() && $block instanceof PreviewFallbackInterface;
// If the content is empty and no placeholder is available, return.
if ($is_content_empty && !$is_placeholder_ready) {
return;
}
$build = [
// @todo Move this to BlockBase in https://www.drupal.org/node/2931040.
'#theme' => 'block',
@ -96,8 +106,11 @@ class BlockComponentRenderArray implements EventSubscriberInterface {
'#base_plugin_id' => $block->getBaseId(),
'#derivative_plugin_id' => $block->getDerivativeId(),
'#weight' => $event->getComponent()->getWeight(),
'content' => $block->build(),
'content' => $content,
];
if ($is_content_empty && $is_placeholder_ready) {
$build['content']['#markup'] = $block->getPreviewFallbackString();
}
$event->setBuild($build);
}
}

View file

@ -7,6 +7,7 @@ use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\field_ui\Form\EntityViewDisplayEditForm;
use Drupal\layout_builder\Entity\LayoutEntityDisplayInterface;
use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
use Drupal\layout_builder\SectionStorageInterface;
/**
@ -48,8 +49,8 @@ class LayoutBuilderEntityViewDisplayForm extends EntityViewDisplayEditForm {
$form = parent::form($form, $form_state);
// Remove the Layout Builder field from the list.
$form['#fields'] = array_diff($form['#fields'], ['layout_builder__layout']);
unset($form['fields']['layout_builder__layout']);
$form['#fields'] = array_diff($form['#fields'], [OverridesSectionStorage::FIELD_NAME]);
unset($form['fields'][OverridesSectionStorage::FIELD_NAME]);
$is_enabled = $this->entity->isLayoutBuilderEnabled();
if ($is_enabled) {
@ -133,7 +134,7 @@ class LayoutBuilderEntityViewDisplayForm extends EntityViewDisplayEditForm {
$entity_type = $this->entityTypeManager->getDefinition($display->getTargetEntityTypeId());
$query = $this->entityTypeManager->getStorage($display->getTargetEntityTypeId())->getQuery()
->exists('layout_builder__layout');
->exists(OverridesSectionStorage::FIELD_NAME);
if ($bundle_key = $entity_type->getKey('bundle')) {
$query->condition($bundle_key, $display->getTargetBundle());
}

View file

@ -6,6 +6,7 @@ use Drupal\Component\Plugin\DerivativeInspectionInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\layout_builder\Entity\LayoutEntityDisplayInterface;
use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
/**
* Methods to help with entities using the layout builder.
@ -65,7 +66,7 @@ trait LayoutEntityHelperTrait {
return $entity->getSections();
}
elseif ($this->isEntityUsingFieldOverride($entity)) {
return $entity->get('layout_builder__layout')->getSections();
return $entity->get(OverridesSectionStorage::FIELD_NAME)->getSections();
}
return NULL;
}
@ -102,7 +103,7 @@ trait LayoutEntityHelperTrait {
* TRUE if the entity is using a field for a layout override.
*/
protected function isEntityUsingFieldOverride(EntityInterface $entity) {
return $entity instanceof FieldableEntityInterface && $entity->hasField('layout_builder__layout');
return $entity instanceof FieldableEntityInterface && $entity->hasField(OverridesSectionStorage::FIELD_NAME);
}
}

View file

@ -45,6 +45,15 @@ class LayoutTempstoreRepository implements LayoutTempstoreRepositoryInterface {
return $section_storage;
}
/**
* {@inheritdoc}
*/
public function has(SectionStorageInterface $section_storage) {
$id = $section_storage->getStorageId();
$tempstore = $this->getTempstore($section_storage)->get($id);
return !empty($tempstore['section_storage']);
}
/**
* {@inheritdoc}
*/

View file

@ -35,6 +35,17 @@ interface LayoutTempstoreRepositoryInterface {
*/
public function set(SectionStorageInterface $section_storage);
/**
* Checks for the existence of a tempstore version of a section storage.
*
* @param \Drupal\layout_builder\SectionStorageInterface $section_storage
* The section storage to check for in tempstore.
*
* @return bool
* TRUE if there is a tempstore version of this section storage.
*/
public function has(SectionStorageInterface $section_storage);
/**
* Removes the tempstore version of a section storage.
*

View file

@ -130,13 +130,22 @@ class ExtraFieldBlock extends BlockBase implements ContextAwarePluginInterface,
// render array. If the hook is invoked the placeholder will be
// replaced.
// @see ::replaceFieldPlaceholder()
'#markup' => new TranslatableMarkup('Placeholder for the "@field" field', ['@field' => $extra_fields['display'][$this->fieldName]['label']]),
'#markup' => $this->getPreviewFallbackString(),
];
}
CacheableMetadata::createFromObject($this)->applyTo($build);
return $build;
}
/**
* {@inheritdoc}
*/
public function getPreviewFallbackString() {
$entity = $this->getEntity();
$extra_fields = $this->entityFieldManager->getExtraFields($entity->getEntityTypeId(), $entity->bundle());
return new TranslatableMarkup('Placeholder for the "@field" field', ['@field' => $extra_fields['display'][$this->fieldName]['label']]);
}
/**
* Replaces all placeholders for a given field.
*

View file

@ -17,7 +17,6 @@ use Drupal\Core\Field\FormatterPluginManager;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\ContextAwarePluginInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Psr\Log\LoggerInterface;
@ -160,13 +159,17 @@ class FieldBlock extends BlockBase implements ContextAwarePluginInterface, Conta
$build = [];
$this->logger->warning('The field "%field" failed to render with the error of "%error".', ['%field' => $this->fieldName, '%error' => $e->getMessage()]);
}
if (!empty($entity->in_preview) && !Element::getVisibleChildren($build)) {
$build['content']['#markup'] = new TranslatableMarkup('Placeholder for the "@field" field', ['@field' => $this->getFieldDefinition()->getLabel()]);
}
CacheableMetadata::createFromObject($this)->applyTo($build);
return $build;
}
/**
* {@inheritdoc}
*/
public function getPreviewFallbackString() {
return new TranslatableMarkup('Placeholder for the "@field" field', ['@field' => $this->getFieldDefinition()->getLabel()]);
}
/**
* {@inheritdoc}
*/

View file

@ -106,7 +106,7 @@ class FieldBlockDeriver extends DeriverBase implements ContainerDeriverInterface
$derivative['default_formatter'] = $field_type_definition['default_formatter'];
}
$derivative['category'] = $this->t('@entity', ['@entity' => $entity_type_labels[$entity_type_id]]);
$derivative['category'] = $this->t('@entity fields', ['@entity' => $entity_type_labels[$entity_type_id]]);
$derivative['admin_label'] = $field_definition->getLabel();

View file

@ -3,11 +3,11 @@
namespace Drupal\layout_builder\Plugin\Derivative;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\layout_builder\Plugin\SectionStorage\SectionStorageLocalTaskProviderInterface;
use Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
@ -28,14 +28,24 @@ class LayoutBuilderLocalTaskDeriver extends DeriverBase implements ContainerDeri
*/
protected $entityTypeManager;
/**
* The section storage manager.
*
* @var \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface
*/
protected $sectionStorageManager;
/**
* Constructs a new LayoutBuilderLocalTaskDeriver.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface $section_storage_manager
* The section storage manager.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
public function __construct(EntityTypeManagerInterface $entity_type_manager, SectionStorageManagerInterface $section_storage_manager) {
$this->entityTypeManager = $entity_type_manager;
$this->sectionStorageManager = $section_storage_manager;
}
/**
@ -43,7 +53,8 @@ class LayoutBuilderLocalTaskDeriver extends DeriverBase implements ContainerDeri
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$container->get('entity_type.manager')
$container->get('entity_type.manager'),
$container->get('plugin.manager.layout_builder.section_storage')
);
}
@ -51,84 +62,13 @@ class LayoutBuilderLocalTaskDeriver extends DeriverBase implements ContainerDeri
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
foreach ($this->getEntityTypesForOverrides() as $entity_type_id => $entity_type) {
// Overrides.
$this->derivatives["layout_builder.overrides.$entity_type_id.view"] = $base_plugin_definition + [
'route_name' => "layout_builder.overrides.$entity_type_id.view",
'weight' => 15,
'title' => $this->t('Layout'),
'base_route' => "entity.$entity_type_id.canonical",
'cache_contexts' => ['layout_builder_is_active:' . $entity_type_id],
];
$this->derivatives["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],
];
$this->derivatives["layout_builder.overrides.$entity_type_id.cancel"] = $base_plugin_definition + [
'route_name' => "layout_builder.overrides.$entity_type_id.cancel",
'title' => $this->t('Cancel Layout'),
'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.
$this->derivatives["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],
];
foreach ($this->sectionStorageManager->getDefinitions() as $plugin_id => $definition) {
$section_storage = $this->sectionStorageManager->loadEmpty($plugin_id);
if ($section_storage instanceof SectionStorageLocalTaskProviderInterface) {
$this->derivatives += $section_storage->buildLocalTasks($base_plugin_definition);
}
}
foreach ($this->getEntityTypesForDefaults() as $entity_type_id => $entity_type) {
// Defaults.
$this->derivatives["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",
];
$this->derivatives["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",
];
$this->derivatives["layout_builder.defaults.$entity_type_id.cancel"] = $base_plugin_definition + [
'route_name' => "layout_builder.defaults.$entity_type_id.cancel",
'title' => $this->t('Cancel Layout'),
'weight' => 5,
'parent_id' => "layout_builder_ui:layout_builder.defaults.$entity_type_id.view",
];
}
return $this->derivatives;
}
/**
* Returns an array of entity types relevant for defaults.
*
* @return \Drupal\Core\Entity\EntityTypeInterface[]
* An array of entity types.
*/
protected function getEntityTypesForDefaults() {
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');
});
}
/**
* Returns an array of entity types relevant for overrides.
*
* @return \Drupal\Core\Entity\EntityTypeInterface[]
* An array of entity types.
*/
protected function getEntityTypesForOverrides() {
return array_filter($this->entityTypeManager->getDefinitions(), function (EntityTypeInterface $entity_type) {
return $entity_type->entityClassImplements(FieldableEntityInterface::class) && $entity_type->hasViewBuilderClass() && $entity_type->hasLinkTemplate('canonical');
});
}
}

View file

@ -32,7 +32,7 @@ use Symfony\Component\Routing\RouteCollection;
* 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 {
class DefaultsSectionStorage extends SectionStorageBase implements ContainerFactoryPluginInterface, DefaultsSectionStorageInterface, SectionStorageLocalTaskProviderInterface {
/**
* The entity type manager.
@ -196,6 +196,32 @@ class DefaultsSectionStorage extends SectionStorageBase implements ContainerFact
}
}
/**
* {@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.cancel"] = $base_plugin_definition + [
'route_name' => "layout_builder.defaults.$entity_type_id.cancel",
'title' => $this->t('Cancel Layout'),
'weight' => 5,
'parent_id' => "layout_builder_ui:layout_builder.defaults.$entity_type_id.view",
];
}
return $local_tasks;
}
/**
* Returns an array of relevant entity types.
*

View file

@ -30,7 +30,14 @@ use Symfony\Component\Routing\RouteCollection;
* experimental modules and development releases of contributed modules.
* See https://www.drupal.org/core/experimental for more information.
*/
class OverridesSectionStorage extends SectionStorageBase implements ContainerFactoryPluginInterface, OverridesSectionStorageInterface {
class OverridesSectionStorage extends SectionStorageBase implements ContainerFactoryPluginInterface, OverridesSectionStorageInterface, SectionStorageLocalTaskProviderInterface {
/**
* The field name used by this storage.
*
* @var string
*/
const FIELD_NAME = 'layout_builder__layout';
/**
* The entity type manager.
@ -127,8 +134,8 @@ class OverridesSectionStorage extends SectionStorageBase implements ContainerFac
if (strpos($id, '.') !== FALSE) {
list($entity_type_id, $entity_id) = explode('.', $id, 2);
$entity = $this->entityTypeManager->getStorage($entity_type_id)->load($entity_id);
if ($entity instanceof FieldableEntityInterface && $entity->hasField('layout_builder__layout')) {
return $entity->get('layout_builder__layout');
if ($entity instanceof FieldableEntityInterface && $entity->hasField(static::FIELD_NAME)) {
return $entity->get(static::FIELD_NAME);
}
}
throw new \InvalidArgumentException(sprintf('The "%s" ID for the "%s" section storage type is invalid', $id, $this->getStorageType()));
@ -157,6 +164,45 @@ class OverridesSectionStorage extends SectionStorageBase implements ContainerFac
}
}
/**
* {@inheritdoc}
*/
public function buildLocalTasks($base_plugin_definition) {
$local_tasks = [];
foreach ($this->getEntityTypes() as $entity_type_id => $entity_type) {
$local_tasks["layout_builder.overrides.$entity_type_id.view"] = $base_plugin_definition + [
'route_name' => "layout_builder.overrides.$entity_type_id.view",
'weight' => 15,
'title' => $this->t('Layout'),
'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.cancel"] = $base_plugin_definition + [
'route_name' => "layout_builder.overrides.$entity_type_id.cancel",
'title' => $this->t('Cancel Layout'),
'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;
}
/**
* Determines if this entity type's ID is stored as an integer.
*

View file

@ -0,0 +1,29 @@
<?php
namespace Drupal\layout_builder\Plugin\SectionStorage;
/**
* Allows section storage plugins to provide local tasks.
*
* @see \Drupal\layout_builder\Plugin\Derivative\LayoutBuilderLocalTaskDeriver
* @see \Drupal\layout_builder\SectionStorageInterface
*
* @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.
*/
interface SectionStorageLocalTaskProviderInterface {
/**
* Provides the local tasks dynamically for Layout Builder plugins.
*
* @param mixed $base_plugin_definition
* The definition of the base plugin.
*
* @return array
* An array of full derivative definitions keyed on derivative ID.
*/
public function buildLocalTasks($base_plugin_definition);
}

View file

@ -356,4 +356,13 @@ class Section {
);
}
/**
* Magic method: Implements a deep clone.
*/
public function __clone() {
foreach ($this->components as $uuid => $component) {
$this->components[$uuid] = clone $component;
}
}
}

View file

@ -111,4 +111,17 @@ trait SectionStorageTrait {
return isset($this->getSections()[$delta]);
}
/**
* Magic method: Implements a deep clone.
*/
public function __clone() {
$sections = $this->getSections();
foreach ($sections as $delta => $item) {
$sections[$delta] = clone $item;
}
$this->setSections($sections);
}
}

View file

@ -0,0 +1,3 @@
# See \Drupal\layout_builder_fieldblock_test\Plugin\Block\FieldBlock.
block.settings.field_block_test:*:*:*:
type: block.settings.field_block:*:*:*

View file

@ -0,0 +1,6 @@
name: 'Layout Builder test'
type: module
description: 'Support module for testing layout building.'
package: Testing
version: VERSION
core: 8.x

View file

@ -0,0 +1,27 @@
<?php
namespace Drupal\layout_builder_fieldblock_test\Plugin\Block;
use Drupal\layout_builder\Plugin\Block\FieldBlock as LayoutBuilderFieldBlock;
/**
* Provides test field block to test with Block UI.
*
* \Drupal\Tests\layout_builder\FunctionalJavascript\FieldBlockTest provides
* test coverage of complex AJAX interactions within certain field blocks.
* layout_builder_plugin_filter_block__block_ui_alter() removes certain blocks
* with 'layout_builder' as the provider. To make these blocks available during
* testing, this plugin uses the same deriver but each derivative will have a
* different provider.
*
* @Block(
* id = "field_block_test",
* deriver = "\Drupal\layout_builder\Plugin\Derivative\FieldBlockDeriver",
* )
*
* @see \Drupal\Tests\layout_builder\FunctionalJavascript\FieldBlockTest
* @see layout_builder_plugin_filter_block__block_ui_alter()
*/
class FieldBlock extends LayoutBuilderFieldBlock {
}

View file

@ -22,6 +22,7 @@ class LayoutBuilderTest extends BrowserTestBase {
'layout_builder_views_test',
'layout_test',
'block',
'block_test',
'node',
'layout_builder_test',
];
@ -90,7 +91,7 @@ class LayoutBuilderTest extends BrowserTestBase {
// The body field is only present once.
$assert_session->elementsCount('css', '.field--name-body', 1);
// The extra field is only present once.
$this->assertTextAppearsOnce('Placeholder for the "Extra label" field');
$assert_session->pageTextContainsOnce('Placeholder for the "Extra label" field');
// Save the defaults.
$assert_session->linkExists('Save Layout');
$this->clickLink('Save Layout');
@ -105,7 +106,7 @@ class LayoutBuilderTest extends BrowserTestBase {
// The body field is only present once.
$assert_session->elementsCount('css', '.field--name-body', 1);
// The extra field is only present once.
$this->assertTextAppearsOnce('Placeholder for the "Extra label" field');
$assert_session->pageTextContainsOnce('Placeholder for the "Extra label" field');
// Add a new block.
$assert_session->linkExists('Add Block');
@ -316,6 +317,11 @@ class LayoutBuilderTest extends BrowserTestBase {
$page->fillField('id', 'myothermenu');
$page->pressButton('Save');
$page->clickLink('Add link');
$page->fillField('title[0][value]', 'My link');
$page->fillField('link[0][uri]', '/');
$page->pressButton('Save');
$this->drupalPostForm('admin/structure/types/manage/bundle_with_section_field/display', ['layout[enabled]' => TRUE], 'Save');
$assert_session->linkExists('Manage layout');
$this->clickLink('Manage layout');
@ -514,13 +520,68 @@ class LayoutBuilderTest extends BrowserTestBase {
}
/**
* Asserts that a text string only appears once on the page.
* Tests the usage of placeholders for empty blocks.
*
* @param string $needle
* The string to look for.
* @see \Drupal\Core\Block\BlockPluginInterface::getPlaceholderString()
* @see \Drupal\layout_builder\EventSubscriber\BlockComponentRenderArray::onBuildRender()
*/
protected function assertTextAppearsOnce($needle) {
$this->assertEquals(1, substr_count($this->getSession()->getPage()->getContent(), $needle), "'$needle' only appears once on the page.");
public function testBlockPlaceholder() {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'administer node display',
]));
$field_ui_prefix = 'admin/structure/types/manage/bundle_with_section_field';
$this->drupalPostForm("$field_ui_prefix/display/default", ['layout[enabled]' => TRUE], 'Save');
// Customize the default view mode.
$this->drupalGet("$field_ui_prefix/display-layout/default");
// Add a block whose content is controlled by state and is empty by default.
$this->clickLink('Add Block');
$this->clickLink('Test block caching');
$page->fillField('settings[label]', 'The block label');
$page->pressButton('Add Block');
$block_content = 'I am content';
$placeholder_content = 'Placeholder for the "The block label" block';
// The block placeholder is displayed and there is no content.
$assert_session->pageTextContains($placeholder_content);
$assert_session->pageTextNotContains($block_content);
// Set block content and reload the page.
\Drupal::state()->set('block_test.content', $block_content);
$this->getSession()->reload();
// The block placeholder is no longer displayed and the content is visible.
$assert_session->pageTextNotContains($placeholder_content);
$assert_session->pageTextContains($block_content);
}
/**
* Tests the Block UI when Layout Builder is installed.
*/
public function testBlockUiListing() {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalLogin($this->drupalCreateUser([
'administer blocks',
]));
$this->drupalGet('admin/structure/block');
$page->clickLink('Place block');
// Ensure that blocks expected to appear are available.
$assert_session->pageTextContains('Test HTML block');
$assert_session->pageTextContains('Block test');
// Ensure that blocks not expected to appear are not available.
$assert_session->pageTextNotContains('Body');
$assert_session->pageTextNotContains('Content fields');
}
}

View file

@ -4,6 +4,7 @@ namespace Drupal\Tests\layout_builder\Functional;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
use Drupal\layout_builder\Section;
use Drupal\layout_builder\SectionComponent;
use Drupal\Tests\BrowserTestBase;
@ -20,13 +21,6 @@ class LayoutSectionTest extends BrowserTestBase {
*/
public static $modules = ['field_ui', 'layout_builder', 'node', 'block_test'];
/**
* The name of the layout section field.
*
* @var string
*/
protected $fieldName = 'layout_builder__layout';
/**
* {@inheritdoc}
*/
@ -226,7 +220,7 @@ class LayoutSectionTest extends BrowserTestBase {
]);
$entity->addTranslation('es', [
'title' => 'Translated node title',
$this->fieldName => [
OverridesSectionStorage::FIELD_NAME => [
[
'section' => new Section('layout_twocol', [], [
'foo' => new SectionComponent('foo', 'first', [
@ -373,7 +367,7 @@ class LayoutSectionTest extends BrowserTestBase {
'value' => 'The node body',
],
],
$this->fieldName => $section_values,
OverridesSectionStorage::FIELD_NAME => $section_values,
]);
}

View file

@ -16,7 +16,14 @@ class FieldBlockTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['block', 'datetime', 'layout_builder', 'user'];
protected static $modules = [
'block',
'datetime',
'layout_builder',
'user',
// See \Drupal\layout_builder_fieldblock_test\Plugin\Block\FieldBlock.
'layout_builder_fieldblock_test',
];
/**
* {@inheritdoc}
@ -67,7 +74,7 @@ class FieldBlockTest extends WebDriverTestBase {
$assert_session->pageTextNotContains('Initial email');
$assert_session->pageTextContains('Date field');
$block_url = 'admin/structure/block/add/field_block%3Auser%3Auser%3Afield_date/classy';
$block_url = 'admin/structure/block/add/field_block_test%3Auser%3Auser%3Afield_date/classy';
$assert_session->linkByHrefExists($block_url);
$this->drupalGet($block_url);

View file

@ -192,8 +192,8 @@ class InlineBlockPrivateFilesTest extends InlineBlockTestBase {
$page = $this->getSession()->getPage();
$page->clickLink('Add Block');
$assert_session->assertWaitOnAjaxRequest();
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '.block-categories details:contains(Create new block)'));
$this->clickLink('Basic block');
$this->assertNotEmpty($assert_session->waitForLink('Create custom block'));
$this->clickLink('Create custom block');
$assert_session->assertWaitOnAjaxRequest();
$assert_session->fieldValueEquals('Title', '');
$page->findField('Title')->setValue($title);

View file

@ -428,4 +428,74 @@ class InlineBlockTest extends InlineBlockTestBase {
$assert_session->pageTextNotContains('You are not authorized to access this page');
}
/**
* Tests the workflow for adding an inline block depending on number of types.
*
* @throws \Behat\Mink\Exception\ElementNotFoundException
* @throws \Behat\Mink\Exception\ExpectationException
*/
public function testAddWorkFlow() {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$type_storage = $this->container->get('entity_type.manager')->getStorage('block_content_type');
foreach ($type_storage->loadByProperties() as $type) {
$type->delete();
}
$this->drupalLogin($this->drupalCreateUser([
'access contextual links',
'configure any layout',
'administer node display',
'administer node fields',
]));
// Enable layout builder and overrides.
$this->drupalPostForm(
static::FIELD_UI_PREFIX . '/display/default',
['layout[enabled]' => TRUE, 'layout[allow_custom]' => TRUE],
'Save'
);
$layout_default_path = 'admin/structure/types/manage/bundle_with_section_field/display-layout/default';
$this->drupalGet($layout_default_path);
// Add a basic block with the body field set.
$page->clickLink('Add Block');
$assert_session->assertWaitOnAjaxRequest();
// Confirm that with no block content types the link does not appear.
$assert_session->linkNotExists('Create custom block');
$this->createBlockContentType('basic', 'Basic block');
$this->drupalGet($layout_default_path);
// Add a basic block with the body field set.
$page->clickLink('Add Block');
$assert_session->assertWaitOnAjaxRequest();
// Confirm with only 1 type the "Create custom block" link goes directly t
// block add form.
$assert_session->linkNotExists('Basic block');
$this->clickLink('Create custom block');
$assert_session->assertWaitOnAjaxRequest();
$assert_session->fieldExists('Title');
$this->createBlockContentType('advanced', 'Advanced block');
$this->drupalGet($layout_default_path);
// Add a basic block with the body field set.
$page->clickLink('Add Block');
// Confirm that, when more than 1 type exists, "Create custom block" shows a
// list of block types.
$assert_session->assertWaitOnAjaxRequest();
$assert_session->linkNotExists('Basic block');
$assert_session->linkNotExists('Advanced block');
$this->clickLink('Create custom block');
$assert_session->assertWaitOnAjaxRequest();
$assert_session->fieldNotExists('Title');
$assert_session->linkExists('Basic block');
$assert_session->linkExists('Advanced block');
$this->clickLink('Advanced block');
$assert_session->assertWaitOnAjaxRequest();
$assert_session->fieldExists('Title');
}
}

View file

@ -71,13 +71,7 @@ abstract class InlineBlockTestBase extends WebDriverTestBase {
],
],
]);
$bundle = BlockContentType::create([
'id' => 'basic',
'label' => 'Basic block',
'revision' => 1,
]);
$bundle->save();
block_content_add_body_field($bundle->id());
$this->createBlockContentType('basic', 'Basic block');
$this->blockStorage = $this->container->get('entity_type.manager')->getStorage('block_content');
}
@ -146,8 +140,8 @@ abstract class InlineBlockTestBase extends WebDriverTestBase {
$page = $this->getSession()->getPage();
$page->clickLink('Add Block');
$assert_session->assertWaitOnAjaxRequest();
$this->assertNotEmpty($assert_session->waitForElementVisible('css', '.block-categories details:contains(Create new block)'));
$this->clickLink('Basic block');
$this->assertNotEmpty($assert_session->waitForLink('Create custom block'));
$this->clickLink('Create custom block');
$assert_session->assertWaitOnAjaxRequest();
$textarea = $assert_session->waitForElement('css', '[name="settings[block_form][body][0][value]"]');
$this->assertNotEmpty($textarea);
@ -219,4 +213,22 @@ abstract class InlineBlockTestBase extends WebDriverTestBase {
}
}
/**
* Creates a block content type.
*
* @param string $id
* The block type id.
* @param string $label
* The block type label.
*/
protected function createBlockContentType($id, $label) {
$bundle = BlockContentType::create([
'id' => $id,
'label' => $label,
'revision' => 1,
]);
$bundle->save();
block_content_add_body_field($bundle->id());
}
}

View file

@ -0,0 +1,94 @@
<?php
namespace Drupal\Tests\layout_builder\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Tests the Layout Builder UI.
*
* @group layout_builder
*/
class LayoutBuilderUiTest extends WebDriverTestBase {
/**
* Path prefix for the field UI for the test bundle.
*
* @var string
*/
const FIELD_UI_PREFIX = 'admin/structure/types/manage/bundle_with_section_field';
public static $modules = [
'layout_builder',
'block',
'node',
];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// @todo The Layout Builder UI relies on local tasks; fix in
// https://www.drupal.org/project/drupal/issues/2917777.
$this->drupalPlaceBlock('local_tasks_block');
$this->createContentType(['type' => 'bundle_with_section_field']);
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'administer node display',
'administer node fields',
]));
}
/**
* Tests the message indicating unsaved changes.
*/
public function testUnsavedChangesMessage() {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
// Enable layout builder.
$this->drupalPostForm(
static::FIELD_UI_PREFIX . '/display/default',
['layout[enabled]' => TRUE],
'Save'
);
// Make and then cancel changes.
$this->assertModifiedLayout(static::FIELD_UI_PREFIX . '/display-layout/default');
$page->clickLink('Cancel Layout');
$assert_session->pageTextNotContains('You have unsaved changes.');
// Make and then save changes.
$this->assertModifiedLayout(static::FIELD_UI_PREFIX . '/display-layout/default');
$page->clickLink('Save Layout');
$assert_session->pageTextNotContains('You have unsaved changes.');
}
/**
* Asserts that modifying a layout works as expected.
*
* @param string $path
* The path to a Layout Builder UI page.
*/
protected function assertModifiedLayout($path) {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalGet($path);
$page->clickLink('Add Section');
$assert_session->assertWaitOnAjaxRequest();
$assert_session->pageTextNotContains('You have unsaved changes.');
$page->clickLink('One column');
$assert_session->assertWaitOnAjaxRequest();
$assert_session->pageTextContainsOnce('You have unsaved changes.');
// Reload the page.
$this->drupalGet($path);
$assert_session->pageTextContainsOnce('You have unsaved changes.');
}
}

View file

@ -230,11 +230,10 @@ class FieldBlockTest extends EntityKernelTestBase {
* @covers ::build
* @dataProvider providerTestBuild
*/
public function testBuild(PromiseInterface $promise, $in_preview, $expected_markup, $log_message = '', $log_arguments = []) {
public function testBuild(PromiseInterface $promise, $expected_markup, $log_message = '', $log_arguments = []) {
$entity = $this->prophesize(FieldableEntityInterface::class);
$field = $this->prophesize(FieldItemListInterface::class);
$entity->get('the_field_name')->willReturn($field->reveal());
$entity->in_preview = $in_preview;
$field->view(Argument::type('array'))->will($promise);
$field_definition = $this->prophesize(FieldDefinitionInterface::class);
@ -269,40 +268,20 @@ class FieldBlockTest extends EntityKernelTestBase {
*/
public function providerTestBuild() {
$data = [];
$data['array, no preview'] = [
$data['array'] = [
new ReturnPromise([['content' => ['#markup' => 'The field value']]]),
FALSE,
'The field value',
];
$data['array, preview'] = [
new ReturnPromise([['content' => ['#markup' => 'The field value']]]),
TRUE,
'The field value',
];
$data['empty array, no preview'] = [
$data['empty array'] = [
new ReturnPromise([[]]),
FALSE,
'',
];
$data['empty array, preview'] = [
new ReturnPromise([[]]),
TRUE,
'Placeholder for the "The Field Label" field',
];
$data['exception, no preview'] = [
$data['exception'] = [
new ThrowPromise(new \Exception('The exception message')),
FALSE,
'',
'The field "%field" failed to render with the error of "%error".',
['%field' => 'the_field_name', '%error' => 'The exception message'],
];
$data['exception, preview'] = [
new ThrowPromise(new \Exception('The exception message')),
TRUE,
'Placeholder for the "The Field Label" field',
'The field "%field" failed to render with the error of "%error".',
['%field' => 'the_field_name', '%error' => 'The exception message'],
];
return $data;
}

View file

@ -2,6 +2,7 @@
namespace Drupal\Tests\layout_builder\Kernel;
use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
use Drupal\layout_builder\Section;
/**
@ -56,7 +57,7 @@ class LayoutBuilderFieldLayoutCompatibilityTest extends LayoutBuilderCompatibili
// Add a layout override.
$this->enableOverrides();
/** @var \Drupal\layout_builder\SectionStorageInterface $field_list */
$field_list = $this->entity->get('layout_builder__layout');
$field_list = $this->entity->get(OverridesSectionStorage::FIELD_NAME);
$field_list->appendSection(new Section('layout_onecol'));
$this->entity->save();

View file

@ -4,6 +4,7 @@ namespace Drupal\Tests\layout_builder\Kernel;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
use Drupal\layout_builder\Section;
/**
@ -35,7 +36,7 @@ class LayoutBuilderInstallTest extends LayoutBuilderCompatibilityTestBase {
// 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->get(OverridesSectionStorage::FIELD_NAME)->appendSection(new Section('layout_onecol'));
$this->entity->save();
// The rendered entity has now changed. The non-configurable field is shown
@ -50,7 +51,7 @@ class LayoutBuilderInstallTest extends LayoutBuilderCompatibilityTestBase {
$this->assertNotEmpty($this->cssSelect('.layout--onecol'));
// Removing the layout restores the original rendering of the entity.
$this->entity->get('layout_builder__layout')->removeSection(0);
$this->entity->get(OverridesSectionStorage::FIELD_NAME)->removeSection(0);
$this->entity->save();
$this->assertFieldAttributes($this->entity, $expected_fields);

View file

@ -4,6 +4,7 @@ namespace Drupal\Tests\layout_builder\Kernel;
use Drupal\entity_test\Entity\EntityTestBaseFieldDisplay;
use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay;
use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
/**
* Tests the field type for Layout Sections.
@ -42,10 +43,10 @@ class LayoutSectionItemListTest extends SectionStorageTestBase {
}, $section_data);
$entity = EntityTestBaseFieldDisplay::create([
'name' => 'The test entity',
'layout_builder__layout' => $section_data,
OverridesSectionStorage::FIELD_NAME => $section_data,
]);
$entity->save();
return $entity->get('layout_builder__layout');
return $entity->get(OverridesSectionStorage::FIELD_NAME);
}
}

View file

@ -137,6 +137,17 @@ abstract class SectionStorageTestBase extends EntityKernelTestBase {
$this->assertSections($expected);
}
/**
* Tests __clone().
*/
public function testClone() {
$this->assertSame([], $this->sectionStorage->getSection(0)->getLayoutSettings());
$new_section_storage = clone $this->sectionStorage;
$new_section_storage->getSection(0)->setLayoutSettings(['asdf' => 'qwer']);
$this->assertSame([], $this->sectionStorage->getSection(0)->getLayoutSettings());
}
/**
* Asserts that the field list has the expected sections.
*

View file

@ -11,6 +11,7 @@ use Drupal\Core\Cache\Cache;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Plugin\Context\ContextHandlerInterface;
use Drupal\Core\Render\PreviewFallbackInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\layout_builder\Access\LayoutPreviewAccessAllowed;
use Drupal\layout_builder\Event\SectionComponentBuildRenderArrayEvent;
@ -252,6 +253,98 @@ class BlockComponentRenderArrayTest extends UnitTestCase {
$this->assertEquals($expected_cache, $result);
}
/**
* @covers ::onBuildRender
*/
public function testOnBuildRenderInPreviewEmptyBuild() {
$block = $this->prophesize(BlockPluginInterface::class)->willImplement(PreviewFallbackInterface::class);
$block->access($this->account->reveal(), TRUE)->shouldNotBeCalled();
$block->getCacheContexts()->willReturn([]);
$block->getCacheTags()->willReturn(['test']);
$block->getCacheMaxAge()->willReturn(Cache::PERMANENT);
$block->getConfiguration()->willReturn([]);
$block->getPluginId()->willReturn('block_plugin_id');
$block->getBaseId()->willReturn('block_plugin_id');
$block->getDerivativeId()->willReturn(NULL);
$placeholder_string = 'The placeholder string';
$block->getPreviewFallbackString()->willReturn($placeholder_string);
$block_content = [];
$block->build()->willReturn($block_content);
$this->blockManager->createInstance('some_block_id', ['id' => 'some_block_id'])->willReturn($block->reveal());
$component = new SectionComponent('some-uuid', 'some-region', ['id' => 'some_block_id']);
$event = new SectionComponentBuildRenderArrayEvent($component, [], TRUE);
$subscriber = new BlockComponentRenderArray($this->account->reveal());
$expected_build = [
'#theme' => 'block',
'#weight' => 0,
'#configuration' => [],
'#plugin_id' => 'block_plugin_id',
'#base_plugin_id' => 'block_plugin_id',
'#derivative_plugin_id' => NULL,
'content' => $block_content,
];
$expected_build['content']['#markup'] = $placeholder_string;
$expected_cache = $expected_build + [
'#cache' => [
'contexts' => [],
'tags' => ['test'],
'max-age' => 0,
],
];
$subscriber->onBuildRender($event);
$result = $event->getBuild();
$this->assertEquals($expected_build, $result);
$event->getCacheableMetadata()->applyTo($result);
$this->assertEquals($expected_cache, $result);
}
/**
* @covers ::onBuildRender
*/
public function testOnBuildRenderEmptyBuild() {
$block = $this->prophesize(BlockPluginInterface::class);
$access_result = AccessResult::allowed();
$block->access($this->account->reveal(), TRUE)->willReturn($access_result)->shouldBeCalled();
$block->getCacheContexts()->willReturn([]);
$block->getCacheTags()->willReturn(['test']);
$block->getCacheMaxAge()->willReturn(Cache::PERMANENT);
$block->getConfiguration()->willReturn([]);
$block->getPluginId()->willReturn('block_plugin_id');
$block->getBaseId()->willReturn('block_plugin_id');
$block->getDerivativeId()->willReturn(NULL);
$block->build()->willReturn([]);
$this->blockManager->createInstance('some_block_id', ['id' => 'some_block_id'])->willReturn($block->reveal());
$component = new SectionComponent('some-uuid', 'some-region', ['id' => 'some_block_id']);
$event = new SectionComponentBuildRenderArrayEvent($component, [], FALSE);
$subscriber = new BlockComponentRenderArray($this->account->reveal());
$expected_build = [];
$expected_cache = $expected_build + [
'#cache' => [
'contexts' => [],
'tags' => ['test'],
'max-age' => -1,
],
];
$subscriber->onBuildRender($event);
$result = $event->getBuild();
$this->assertEquals($expected_build, $result);
$event->getCacheableMetadata()->applyTo($result);
$this->assertEquals($expected_cache, $result);
}
/**
* @covers ::onBuildRender
*/

View file

@ -16,6 +16,7 @@ class LayoutTempstoreRepositoryTest extends UnitTestCase {
/**
* @covers ::get
* @covers ::has
*/
public function testGetEmptyTempstore() {
$section_storage = $this->prophesize(SectionStorageInterface::class);
@ -30,12 +31,15 @@ class LayoutTempstoreRepositoryTest extends UnitTestCase {
$repository = new LayoutTempstoreRepository($tempstore_factory->reveal());
$this->assertFalse($repository->has($section_storage->reveal()));
$result = $repository->get($section_storage->reveal());
$this->assertSame($section_storage->reveal(), $result);
}
/**
* @covers ::get
* @covers ::has
*/
public function testGetLoadedTempstore() {
$section_storage = $this->prophesize(SectionStorageInterface::class);
@ -50,6 +54,8 @@ class LayoutTempstoreRepositoryTest extends UnitTestCase {
$repository = new LayoutTempstoreRepository($tempstore_factory->reveal());
$this->assertTrue($repository->has($section_storage->reveal()));
$result = $repository->get($section_storage->reveal());
$this->assertSame($tempstore_section_storage->reveal(), $result);
$this->assertNotSame($section_storage->reveal(), $result);

View file

@ -112,13 +112,13 @@ class OverridesSectionStorageTest extends UnitTestCase {
$entity_storage = $this->prophesize(EntityStorageInterface::class);
$entity_without_layout = $this->prophesize(FieldableEntityInterface::class);
$entity_without_layout->hasField('layout_builder__layout')->willReturn(FALSE);
$entity_without_layout->get('layout_builder__layout')->shouldNotBeCalled();
$entity_without_layout->hasField(OverridesSectionStorage::FIELD_NAME)->willReturn(FALSE);
$entity_without_layout->get(OverridesSectionStorage::FIELD_NAME)->shouldNotBeCalled();
$entity_storage->load('entity_without_layout')->willReturn($entity_without_layout->reveal());
$entity_with_layout = $this->prophesize(FieldableEntityInterface::class);
$entity_with_layout->hasField('layout_builder__layout')->willReturn(TRUE);
$entity_with_layout->get('layout_builder__layout')->willReturn('the_return_value');
$entity_with_layout->hasField(OverridesSectionStorage::FIELD_NAME)->willReturn(TRUE);
$entity_with_layout->get(OverridesSectionStorage::FIELD_NAME)->willReturn('the_return_value');
$entity_storage->load('entity_with_layout')->willReturn($entity_with_layout->reveal());
$this->entityTypeManager->getStorage($expected_entity_type_id)->willReturn($entity_storage->reveal());

View file

@ -231,6 +231,7 @@ class SectionRenderTest extends UnitTestCase {
* @covers ::toRenderArray
*/
public function testContextAwareBlock() {
$block_content = ['#markup' => 'The block content.'];
$render_array = [
'#theme' => 'block',
'#weight' => 0,
@ -238,7 +239,7 @@ class SectionRenderTest extends UnitTestCase {
'#plugin_id' => 'block_plugin_id',
'#base_plugin_id' => 'block_plugin_id',
'#derivative_plugin_id' => NULL,
'content' => [],
'content' => $block_content,
'#cache' => [
'contexts' => [],
'tags' => [],
@ -251,7 +252,7 @@ class SectionRenderTest extends UnitTestCase {
$access_result = AccessResult::allowed();
$block->access($this->account->reveal(), TRUE)->willReturn($access_result);
$block->build()->willReturn([]);
$block->build()->willReturn($block_content);
$block->getCacheContexts()->willReturn([]);
$block->getCacheTags()->willReturn([]);
$block->getCacheMaxAge()->willReturn(Cache::PERMANENT);