Unverified Commit 53f19807 authored by larowlan's avatar larowlan

Issue #2992410 by tim.plunkett, scottsawyer, sugaroverflow, phenaproxima:...

Issue #2992410 by tim.plunkett, scottsawyer, sugaroverflow, phenaproxima: Provide placeholders for empty blocks (for example, an empty Views listing)
parent 530f7ac5
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Plugin\PluginWithFormsInterface; use Drupal\Core\Plugin\PluginWithFormsInterface;
use Drupal\Core\Plugin\PluginWithFormsTrait; use Drupal\Core\Plugin\PluginWithFormsTrait;
use Drupal\Core\Render\PlaceholderInterface;
use Drupal\Core\Session\AccountInterface; use Drupal\Core\Session\AccountInterface;
use Drupal\Component\Transliteration\TransliterationInterface; use Drupal\Component\Transliteration\TransliterationInterface;
...@@ -23,7 +24,7 @@ ...@@ -23,7 +24,7 @@
* *
* @ingroup block_api * @ingroup block_api
*/ */
abstract class BlockBase extends ContextAwarePluginBase implements BlockPluginInterface, PluginWithFormsInterface { abstract class BlockBase extends ContextAwarePluginBase implements BlockPluginInterface, PluginWithFormsInterface, PlaceholderInterface {
use ContextAwarePluginAssignmentTrait; use ContextAwarePluginAssignmentTrait;
use MessengerTrait; use MessengerTrait;
...@@ -252,6 +253,13 @@ public function getMachineNameSuggestion() { ...@@ -252,6 +253,13 @@ public function getMachineNameSuggestion() {
return $transliterated; return $transliterated;
} }
/**
* {@inheritdoc}
*/
public function getPlaceholderString() {
return $this->t('Placeholder for the "@block" block', ['@block' => $this->label()]);
}
/** /**
* Wraps the transliteration service. * Wraps the transliteration service.
* *
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Entity\EntityDisplayBase; use Drupal\Core\Entity\EntityDisplayBase;
use Drupal\Core\Render\Element;
use Drupal\Core\TypedData\TranslatableInterface as TranslatableDataInterface; use Drupal\Core\TypedData\TranslatableInterface as TranslatableDataInterface;
/** /**
...@@ -269,7 +270,7 @@ public function buildMultiple(array $entities) { ...@@ -269,7 +270,7 @@ public function buildMultiple(array $entities) {
foreach ($entities as $id => $entity) { foreach ($entities as $id => $entity) {
// Assign the configured weights. // Assign the configured weights.
foreach ($this->getComponents() as $name => $options) { foreach ($this->getComponents() as $name => $options) {
if (isset($build_list[$id][$name])) { if (isset($build_list[$id][$name]) && !Element::isEmpty($build_list[$id][$name])) {
$build_list[$id][$name]['#weight'] = $options['weight']; $build_list[$id][$name]['#weight'] = $options['weight'];
} }
} }
......
<?php
namespace Drupal\Core\Render;
/**
* Allows an element to provide a placeholder representation of itself.
*/
interface PlaceholderInterface {
/**
* Returns a string to be used as a placeholder.
*
* This is typically used when an element has no output and must be displayed,
* for example during configuration.
*
* @return string|\Drupal\Core\StringTranslation\TranslatableMarkup
* A placeholder string for this element.
*/
public function getPlaceholderString();
}
...@@ -5,6 +5,8 @@ ...@@ -5,6 +5,8 @@
use Drupal\block_content\Access\RefinableDependentAccessInterface; use Drupal\block_content\Access\RefinableDependentAccessInterface;
use Drupal\Core\Access\AccessResult; use Drupal\Core\Access\AccessResult;
use Drupal\Core\Block\BlockPluginInterface; use Drupal\Core\Block\BlockPluginInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\PlaceholderInterface;
use Drupal\Core\Session\AccountInterface; use Drupal\Core\Session\AccountInterface;
use Drupal\layout_builder\Access\LayoutPreviewAccessAllowed; use Drupal\layout_builder\Access\LayoutPreviewAccessAllowed;
use Drupal\layout_builder\Event\SectionComponentBuildRenderArrayEvent; use Drupal\layout_builder\Event\SectionComponentBuildRenderArrayEvent;
...@@ -98,6 +100,9 @@ public function onBuildRender(SectionComponentBuildRenderArrayEvent $event) { ...@@ -98,6 +100,9 @@ public function onBuildRender(SectionComponentBuildRenderArrayEvent $event) {
'#weight' => $event->getComponent()->getWeight(), '#weight' => $event->getComponent()->getWeight(),
'content' => $block->build(), 'content' => $block->build(),
]; ];
if ($event->inPreview() && Element::isEmpty($build['content']) && $block instanceof PlaceholderInterface) {
$build['content']['#markup'] = $block->getPlaceholderString();
}
$event->setBuild($build); $event->setBuild($build);
} }
} }
......
...@@ -130,13 +130,22 @@ public function build() { ...@@ -130,13 +130,22 @@ public function build() {
// render array. If the hook is invoked the placeholder will be // render array. If the hook is invoked the placeholder will be
// replaced. // replaced.
// @see ::replaceFieldPlaceholder() // @see ::replaceFieldPlaceholder()
'#markup' => new TranslatableMarkup('Placeholder for the "@field" field', ['@field' => $extra_fields['display'][$this->fieldName]['label']]), '#markup' => $this->getPlaceholderString(),
]; ];
} }
CacheableMetadata::createFromObject($this)->applyTo($build); CacheableMetadata::createFromObject($this)->applyTo($build);
return $build; return $build;
} }
/**
* {@inheritdoc}
*/
public function getPlaceholderString() {
$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. * Replaces all placeholders for a given field.
* *
......
...@@ -17,7 +17,6 @@ ...@@ -17,7 +17,6 @@
use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\ContextAwarePluginInterface; use Drupal\Core\Plugin\ContextAwarePluginInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Session\AccountInterface; use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\Core\StringTranslation\TranslatableMarkup;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
...@@ -160,13 +159,17 @@ public function build() { ...@@ -160,13 +159,17 @@ public function build() {
$build = []; $build = [];
$this->logger->warning('The field "%field" failed to render with the error of "%error".', ['%field' => $this->fieldName, '%error' => $e->getMessage()]); $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); CacheableMetadata::createFromObject($this)->applyTo($build);
return $build; return $build;
} }
/**
* {@inheritdoc}
*/
public function getPlaceholderString() {
return new TranslatableMarkup('Placeholder for the "@field" field', ['@field' => $this->getFieldDefinition()->getLabel()]);
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
......
...@@ -22,6 +22,7 @@ class LayoutBuilderTest extends BrowserTestBase { ...@@ -22,6 +22,7 @@ class LayoutBuilderTest extends BrowserTestBase {
'layout_builder_views_test', 'layout_builder_views_test',
'layout_test', 'layout_test',
'block', 'block',
'block_test',
'node', 'node',
'layout_builder_test', 'layout_builder_test',
]; ];
...@@ -482,6 +483,49 @@ public function testDeletedView() { ...@@ -482,6 +483,49 @@ public function testDeletedView() {
$assert_session->pageTextNotContains('Test Block View'); $assert_session->pageTextNotContains('Test Block View');
} }
/**
* Tests the usage of placeholders for empty blocks.
*
* @see \Drupal\Core\Block\BlockPluginInterface::getPlaceholderString()
* @see \Drupal\layout_builder\EventSubscriber\BlockComponentRenderArray::onBuildRender()
*/
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);
}
/** /**
* Asserts that a text string only appears once on the page. * Asserts that a text string only appears once on the page.
* *
......
...@@ -230,11 +230,10 @@ protected function getTestBlock(ProphecyInterface $entity_prophecy, array $confi ...@@ -230,11 +230,10 @@ protected function getTestBlock(ProphecyInterface $entity_prophecy, array $confi
* @covers ::build * @covers ::build
* @dataProvider providerTestBuild * @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); $entity = $this->prophesize(FieldableEntityInterface::class);
$field = $this->prophesize(FieldItemListInterface::class); $field = $this->prophesize(FieldItemListInterface::class);
$entity->get('the_field_name')->willReturn($field->reveal()); $entity->get('the_field_name')->willReturn($field->reveal());
$entity->in_preview = $in_preview;
$field->view(Argument::type('array'))->will($promise); $field->view(Argument::type('array'))->will($promise);
$field_definition = $this->prophesize(FieldDefinitionInterface::class); $field_definition = $this->prophesize(FieldDefinitionInterface::class);
...@@ -269,40 +268,20 @@ public function testBuild(PromiseInterface $promise, $in_preview, $expected_mark ...@@ -269,40 +268,20 @@ public function testBuild(PromiseInterface $promise, $in_preview, $expected_mark
*/ */
public function providerTestBuild() { public function providerTestBuild() {
$data = []; $data = [];
$data['array, no preview'] = [ $data['array'] = [
new ReturnPromise([['content' => ['#markup' => 'The field value']]]), 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', 'The field value',
]; ];
$data['empty array, no preview'] = [ $data['empty array'] = [
new ReturnPromise([[]]), new ReturnPromise([[]]),
FALSE,
'', '',
]; ];
$data['empty array, preview'] = [ $data['exception'] = [
new ReturnPromise([[]]),
TRUE,
'Placeholder for the "The Field Label" field',
];
$data['exception, no preview'] = [
new ThrowPromise(new \Exception('The exception message')), new ThrowPromise(new \Exception('The exception message')),
FALSE,
'', '',
'The field "%field" failed to render with the error of "%error".', 'The field "%field" failed to render with the error of "%error".',
['%field' => 'the_field_name', '%error' => 'The exception message'], ['%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; return $data;
} }
......
...@@ -105,6 +105,13 @@ public function defaultConfiguration() { ...@@ -105,6 +105,13 @@ public function defaultConfiguration() {
return ['views_label' => '']; return ['views_label' => ''];
} }
/**
* {@inheritdoc}
*/
public function getPlaceholderString() {
return $this->t('Placeholder for the "@view" views block', ['@view' => $this->view->storage->label()]);
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment