Commit d89c1f70 authored by alexpott's avatar alexpott

Issue #2289917 by Wim Leers, mdrummond, lauriii, Manuel Garcia, emma.maria,...

Issue #2289917 by Wim Leers, mdrummond, lauriii, Manuel Garcia, emma.maria, Scionar, davidhernandez: Convert "messages" page element into blocks
parent 0fd920cb
......@@ -500,25 +500,6 @@ function template_preprocess_datetime_wrapper(&$variables) {
$variables['content'] = $element['#children'];
}
/**
* Prepares variables for status message templates.
*
* Default template: status-messages.html.twig.
*
* @param array $variables
* An associative array containing:
* - display: (optional) May have a value of 'status' or 'error' when only
* displaying messages of that specific type.
*/
function template_preprocess_status_messages(&$variables) {
$variables['message_list'] = drupal_get_messages($variables['display']);
$variables['status_headings'] = array(
'status' => t('Status message'),
'error' => t('Error message'),
'warning' => t('Warning message'),
);
}
/**
* Prepares variables for links templates.
*
......@@ -1376,7 +1357,6 @@ function template_preprocess_page(&$variables) {
$site_config = \Drupal::config('system.site');
// Move some variables to the top level for themer convenience and template cleanliness.
$variables['show_messages'] = $variables['page']['#show_messages'];
$variables['title'] = $variables['page']['#title'];
foreach (system_region_list(\Drupal::theme()->getActiveTheme()->getName()) as $region_key => $region_name) {
......@@ -1414,13 +1394,6 @@ function template_preprocess_page(&$variables) {
if ($node = \Drupal::routeMatch()->getParameter('node')) {
$variables['node'] = $node;
}
// Prepare render array for messages. drupal_get_messages() is called later,
// when this variable is rendered in a theme function or template file.
$variables['messages'] = array(
'#theme' => 'status_messages',
'#access' => $variables['show_messages'],
);
}
/**
......@@ -1763,7 +1736,7 @@ function drupal_common_theme() {
'render element' => 'element',
),
'status_messages' => array(
'variables' => array('display' => NULL),
'variables' => ['status_headings' => [], 'message_list' => NULL],
),
'links' => array(
'variables' => array('links' => array(), 'attributes' => array('class' => array('links')), 'heading' => array(), 'set_active_class' => FALSE),
......
......@@ -35,7 +35,7 @@ trait CommandWithAttachedAssetsTrait {
protected function getRenderedContent() {
$this->attachedAssets = new AttachedAssets();
if (is_array($this->content)) {
$html = \Drupal::service('renderer')->render($this->content);
$html = \Drupal::service('renderer')->renderRoot($this->content);
$this->attachedAssets = AttachedAssets::createFromRenderArray($this->content);
return $html;
}
......
<?php
/**
* @file
* Contains \Drupal\Core\Block\MessagesBlockPluginInterface.
*/
namespace Drupal\Core\Block;
/**
* The interface for "messages" (#type => status_messages) blocks.
*
* @see drupal_set_message()
* @see drupal_get_message()
* @see \Drupal\Core\Render\Element\StatusMessages
* @see \Drupal\block\Plugin\DisplayVariant\BlockPageVariant
*
* @ingroup block_api
*/
interface MessagesBlockPluginInterface extends BlockPluginInterface { }
......@@ -11,7 +11,10 @@
* Defines a page display variant annotation object.
*
* Page display variants are a specific type of display variant, intended to
* render the main content of a page.
* render entire pages. They must render the crucial parts of a page, which are:
* - the title
* - the main content
* - any messages (#type => status_messages)
*
* @see \Drupal\Core\Display\VariantInterface
* @see \Drupal\Core\Display\PageVariantInterface
......
......@@ -465,6 +465,7 @@ public function rebuildThemeData() {
'secondary_menu' => 'Secondary menu',
'footer' => 'Footer',
'highlighted' => 'Highlighted',
'messages' => 'Messages',
'help' => 'Help',
'page_top' => 'Page top',
'page_bottom' => 'Page bottom',
......
......@@ -49,6 +49,12 @@ public function renderBarePage(array $content, $title, $page_theme_property, arr
] + $page_additions,
];
// For backwards compatibility.
// @todo In Drupal 9, add a $show_messages function parameter.
if (!isset($page_additions['#show_messages']) || $page_additions['#show_messages'] === TRUE) {
$html['page']['messages'] = ['#type' => 'status_messages'];
}
// We must first render the contents of the html.html.twig template, see
// \Drupal\Core\Render\MainContent\HtmlRenderer::renderResponse() for more
// information about this; the exact same pattern is used there and
......
......@@ -21,7 +21,6 @@ class Page extends RenderElement {
*/
public function getInfo() {
return array(
'#show_messages' => TRUE,
'#theme' => 'page',
'#title' => '',
);
......
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\StatusMessages.
*/
namespace Drupal\Core\Render\Element;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\Site\Settings;
/**
* Provides a messages element.
*
* @RenderElement("status_messages")
*/
class StatusMessages extends RenderElement {
/**
* {@inheritdoc}
*
* Generate the placeholder in a #pre_render callback, because the hash salt
* needs to be accessed, which may not yet be available when this is called.
*/
public function getInfo() {
return [
// May have a value of 'status' or 'error' when only displaying messages
// of that specific type.
'#display' => NULL,
'#pre_render' => [
get_class() . '::generatePlaceholder',
],
];
}
/**
* #pre_render callback to generate a placeholder.
*
* Ensures the same token is used for all instances, hence resulting in the
* same placeholder for all places rendering the status messages for this
* request (e.g. in multiple blocks). This ensures we can put the rendered
* messages in all placeholders in one go.
* Also ensures the same context key is used for the #post_render_cache
* property, this ensures that if status messages are rendered multiple times,
* their individual (but identical!) #post_render_cache properties are merged,
* ensuring the callback is only invoked once.
*
* @see ::renderMessages()
* @param array $element
* A renderable array.
*
* @return array
* The updated renderable array containing the placeholder.
*/
public static function generatePlaceholder(array $element) {
$plugin_id = 'status_messages';
$callback = get_class() . '::renderMessages';
try {
$hash_salt = Settings::getHashSalt();
}
catch (\RuntimeException $e) {
// Status messages are also shown during the installer, at which time no
// hash salt is defined yet.
$hash_salt = Crypt::randomBytes(8);
}
$key = $plugin_id . $element['#display'];
$context = [
'display' => $element['#display'],
'token' => Crypt::hmacBase64($key, $hash_salt),
];
$placeholder = static::renderer()->generateCachePlaceholder($callback, $context);
$element['#post_render_cache'] = [
$callback => [
$key => $context,
],
];
$element['#markup'] = $placeholder;
return $element;
}
/**
* #post_render_cache callback; replaces placeholder with messages.
*
* Note: this is designed to replace all #post_render_cache placeholders for
* messages in a single #post_render_cache callback; hence all placeholders
* must be identical.
*
* @see ::getInfo()
*
* @param array $element
* The renderable array that contains the to be replaced placeholder.
* @param array $context
* An array with any context information.
*
* @return array
* A renderable array containing the messages.
*/
public static function renderMessages(array $element, array $context) {
$renderer = static::renderer();
// Render the messages.
$messages = [
'#theme' => 'status_messages',
// @todo Improve when https://www.drupal.org/node/2278383 lands.
'#message_list' => drupal_get_messages($context['display']),
'#status_headings' => [
'status' => t('Status message'),
'error' => t('Error message'),
'warning' => t('Warning message'),
],
];
$markup = $renderer->render($messages);
// Replace the placeholder.
$callback = get_class() . '::renderMessages';
$placeholder = $renderer->generateCachePlaceholder($callback, $context);
$element['#markup'] = str_replace($placeholder, $markup, $element['#markup']);
$element = $renderer->mergeBubbleableMetadata($element, $messages);
return $element;
}
/**
* Wraps the renderer.
*
* @return \Drupal\Core\Render\RendererInterface
*/
protected static function renderer() {
return \Drupal::service('renderer');
}
}
......@@ -71,7 +71,7 @@ public function renderResponse(array $main_content, Request $request, RouteMatch
// replace the element making the Ajax call. The default 'replaceWith'
// behavior can be changed with #ajax['method'].
$response->addCommand(new InsertCommand(NULL, $html));
$status_messages = array('#theme' => 'status_messages');
$status_messages = array('#type' => 'status_messages');
$output = $this->drupalRenderRoot($status_messages);
if (!empty($output)) {
$response->addCommand(new PrependCommand(NULL, $output));
......
......@@ -39,7 +39,13 @@ public function setMainContent(array $main_content) {
*/
public function build() {
$build = [
'content' => $this->mainContent,
'content' => [
'main_content' => $this->mainContent,
'messages' => [
'#type' => 'status_messages',
'#weight' => -1000,
],
],
];
return $build;
}
......
......@@ -11,6 +11,7 @@
use Drupal\block\Event\BlockContextEvent;
use Drupal\block\Event\BlockEvents;
use Drupal\Core\Block\MainContentBlockPluginInterface;
use Drupal\Core\Block\MessagesBlockPluginInterface;
use Drupal\Core\Display\PageVariantInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityViewBuilderInterface;
......@@ -22,6 +23,14 @@
/**
* Provides a page display variant that decorates the main content with blocks.
*
* To ensure essential information is displayed, each essential part of a page
* has a corresponding block plugin interface, so that BlockPageVariant can
* automatically provide a fallback in case no block for each of these
* interfaces is placed.
*
* @see \Drupal\Core\Block\MainContentBlockPluginInterface
* @see \Drupal\Core\Block\MessagesBlockPluginInterface
*
* @PageDisplayVariant(
* id = "block_page",
* admin_label = @Translation("Page with blocks")
......@@ -110,8 +119,9 @@ public function setMainContent(array $main_content) {
* {@inheritdoc}
*/
public function build() {
// Track whether a block that shows the main content is displayed or not.
// Track whether blocks showing the main content and messages are displayed.
$main_content_block_displayed = FALSE;
$messages_block_displayed = FALSE;
$build = [
'#cache' => [
......@@ -128,6 +138,9 @@ public function build() {
$block_plugin->setMainContent($this->mainContent);
$main_content_block_displayed = TRUE;
}
elseif ($block_plugin instanceof MessagesBlockPluginInterface) {
$messages_block_displayed = TRUE;
}
$build[$region][$key] = $this->blockViewBuilder->view($block);
}
if (!empty($build[$region])) {
......@@ -144,6 +157,14 @@ public function build() {
$build['content']['system_main'] = $this->mainContent;
}
// If no block displays status messages, still render them.
if (!$messages_block_displayed) {
$build['content']['messages'] = [
'#weight' => -1000,
'#type' => 'status_messages',
];
}
return $build;
}
......
......@@ -70,23 +70,28 @@ public function setUpDisplayVariant($configuration = array(), $definition = arra
public function providerBuild() {
$blocks_config = array(
'block1' => array(
'top', FALSE,
// region, is main content block, is messages block
'top', FALSE, FALSE,
),
// Test multiple blocks in the same region.
'block2' => array(
'bottom', FALSE,
'bottom', FALSE, FALSE,
),
'block3' => array(
'bottom', FALSE,
'bottom', FALSE, FALSE,
),
// Test a block implementing MainContentBlockPluginInterface.
'block4' => array(
'center', TRUE,
'center', TRUE, FALSE,
),
// Test a block implementing MessagesBlockPluginInterface.
'block5' => array(
'center', FALSE, TRUE,
),
);
$test_cases = [];
$test_cases[] = [$blocks_config, 4,
$test_cases[] = [$blocks_config, 5,
[
'#cache' => [
'tags' => [
......@@ -100,6 +105,7 @@ public function providerBuild() {
// The main content was rendered via a block.
'center' => [
'block4' => [],
'block5' => [],
'#sorted' => TRUE,
],
'bottom' => [
......@@ -109,6 +115,37 @@ public function providerBuild() {
],
],
];
unset($blocks_config['block5']);
$test_cases[] = [$blocks_config, 4,
[
'#cache' => [
'tags' => [
'config:block_list',
],
],
'top' => [
'block1' => [],
'#sorted' => TRUE,
],
'center' => [
'block4' => [],
'#sorted' => TRUE,
],
'bottom' => [
'block2' => [],
'block3' => [],
'#sorted' => TRUE,
],
// The messages are rendered via the fallback in case there is no block
// rendering the main content.
'content' => [
'messages' => [
'#weight' => -1000,
'#type' => 'status_messages',
],
],
],
];
unset($blocks_config['block4']);
$test_cases[] = [$blocks_config, 3,
[
......@@ -126,10 +163,14 @@ public function providerBuild() {
'block3' => [],
'#sorted' => TRUE,
],
// The main content was rendered via the fallback in case there is no
// block rendering the main content.
// The main content & messages are rendered via the fallback in case
// there are no blocks rendering them.
'content' => [
'system_main' => ['#markup' => 'Hello kittens!'],
'messages' => [
'#weight' => -1000,
'#type' => 'status_messages',
],
],
],
];
......@@ -150,11 +191,12 @@ public function testBuild(array $blocks_config, $visible_block_count, array $exp
$blocks = ['top' => [], 'center' => [], 'bottom' => []];
$block_plugin = $this->getMock('Drupal\Core\Block\BlockPluginInterface');
$main_content_block_plugin = $this->getMock('Drupal\Core\Block\MainContentBlockPluginInterface');
$messages_block_plugin = $this->getMock('Drupal\Core\Block\MessagesBlockPluginInterface');
foreach ($blocks_config as $block_id => $block_config) {
$block = $this->getMock('Drupal\block\BlockInterface');
$block->expects($this->atLeastOnce())
->method('getPlugin')
->willReturn($block_config[1] ? $main_content_block_plugin : $block_plugin);
->willReturn($block_config[1] ? $main_content_block_plugin : ($block_config[2] ? $messages_block_plugin : $block_plugin));
$blocks[$block_config[0]][$block_id] = $block;
}
......@@ -187,6 +229,10 @@ public function testBuildWithoutMainContent() {
],
'content' => [
'system_main' => [],
'messages' => [
'#weight' => -1000,
'#type' => 'status_messages',
],
],
];
$this->assertSame($expected, $display_variant->build());
......
......@@ -227,7 +227,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
if ($form_state->getErrors()) {
unset($form['#prefix'], $form['#suffix']);
$form['status_messages'] = [
'#theme' => 'status_messages',
'#type' => 'status_messages',
'#weight' => -10,
];
$response->addCommand(new HtmlCommand('#editor-image-dialog-form', $form));
......
......@@ -86,7 +86,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
if ($form_state->getErrors()) {
unset($form['#prefix'], $form['#suffix']);
$form['status_messages'] = [
'#theme' => 'status_messages',
'#type' => 'status_messages',
'#weight' => -10,
];
$response->addCommand(new HtmlCommand('#editor-link-dialog-form', $form));
......
......@@ -38,8 +38,8 @@ public function upload(Request $request) {
// Invalid request.
drupal_set_message(t('An unrecoverable error occurred. The uploaded file likely exceeded the maximum file size (@size) that this server supports.', array('@size' => format_size(file_upload_max_size()))), 'error');
$response = new AjaxResponse();
$status_messages = array('#theme' => 'status_messages');
return $response->addCommand(new ReplaceCommand(NULL, drupal_render($status_messages)));
$status_messages = array('#type' => 'status_messages');
return $response->addCommand(new ReplaceCommand(NULL, $this->renderer->renderRoot($status_messages)));
}
try {
......@@ -53,8 +53,8 @@ public function upload(Request $request) {
// Invalid form_build_id.
drupal_set_message(t('An unrecoverable error occurred. Use of this form has expired. Try reloading the page and submitting again.'), 'error');
$response = new AjaxResponse();
$status_messages = array('#theme' => 'status_messages');
return $response->addCommand(new ReplaceCommand(NULL, drupal_render($status_messages)));
$status_messages = array('#type' => 'status_messages');
return $response->addCommand(new ReplaceCommand(NULL, $this->renderer->renderRoot($status_messages)));
}
// Get the current element and count the number of files.
......@@ -76,9 +76,9 @@ public function upload(Request $request) {
$form['#suffix'] .= '<span class="ajax-new-content"></span>';
}
$status_messages = array('#theme' => 'status_messages');
$form['#prefix'] .= drupal_render($status_messages);
$output = drupal_render($form);
$status_messages = array('#type' => 'status_messages');
$form['#prefix'] .= $this->renderer->renderRoot($status_messages);
$output = $this->renderer->renderRoot($form);
$response = new AjaxResponse();
$response->setAttachments($form['#attached']);
......
......@@ -9,6 +9,7 @@
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Form\FormState;
use Drupal\Core\Render\RendererInterface;
use Drupal\user\PrivateTempStoreFactory;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
......@@ -47,6 +48,13 @@ class QuickEditController extends ControllerBase {
*/
protected $editorSelector;
/**
* The renderer.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* Constructs a new QuickEditController.
*
......@@ -56,11 +64,14 @@ class QuickEditController extends ControllerBase {
* The in-place editing metadata generator.
* @param \Drupal\quickedit\EditorSelectorInterface $editor_selector
* The in-place editor selector.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer.
*/
public function __construct(PrivateTempStoreFactory $temp_store_factory, MetadataGeneratorInterface $metadata_generator, EditorSelectorInterface $editor_selector) {
public function __construct(PrivateTempStoreFactory $temp_store_factory, MetadataGeneratorInterface $metadata_generator, EditorSelectorInterface $editor_selector, RendererInterface $renderer) {
$this->tempStoreFactory = $temp_store_factory;
$this->metadataGenerator = $metadata_generator;
$this->editorSelector = $editor_selector;
$this->renderer = $renderer;
}
/**
......@@ -70,7 +81,8 @@ public static function create(ContainerInterface $container) {
return new static(
$container->get('user.private_tempstore'),
$container->get('quickedit.metadata.generator'),
$container->get('quickedit.editor.selector')
$container->get('quickedit.editor.selector'),
$container->get('renderer')
);
}
......@@ -204,7 +216,7 @@ public function fieldForm(EntityInterface $entity, $field_name, $langcode, $view
$response->addCommand(new FieldFormSavedCommand($output, $other_view_modes));
}
else {
$output = drupal_render($form);
$output = $this->renderer->renderRoot($form);
// When working with a hidden form, we don't want its CSS/JS to be loaded.
if ($request->request->get('nocssjs') !== 'true') {
$response->setAttachments($form['#attached']);
......@@ -214,9 +226,9 @@ public function fieldForm(EntityInterface $entity, $field_name, $langcode, $view
$errors = $form_state->getErrors();
if (count($errors)) {
$status_messages = array(
'#theme' => 'status_messages'
'#type' => 'status_messages'
);
$response->addCommand(new FieldFormValidationErrorsCommand(drupal_render($status_messages)));
$response->addCommand(new FieldFormValidationErrorsCommand($this->renderer->renderRoot($status_messages)));
}
}
......@@ -263,7 +275,7 @@ protected function renderField(EntityInterface $entity, $field_name, $langcode,
$output = $this->moduleHandler()->invoke($module, 'quickedit_render_field', $args);
}
return drupal_render($output);
return $this->renderer->renderRoot($output);
}
/**
......
......@@ -560,7 +560,7 @@ protected function registerStreamWrapper($scheme, $class, $type = StreamWrapperI
* The rendered string output (typically HTML).
*/
protected function render(array &$elements) {
$content = drupal_render($elements);
$content = $this->container->get('renderer')->renderRoot($elements);
drupal_process_attached($elements);
$this->setRawContent($content);
$this->verbose('<pre style="white-space: pre-wrap">' . String::checkPlain($content));
......
......@@ -1991,7 +1991,7 @@ function hook_display_variant_plugin_alter(array &$definitions) {
* 'wrapper' method and return HTML markup. This is not the case if you return
* commands, but if you would like to show status messages, you can add
* @code
* array('#theme' => 'status_messages')
* array('#type' => 'status_messages')
* @endcode
* to a render array, use drupal_render() to render it, and add a command to
* place the messages in an appropriate location.
......
......@@ -153,8 +153,8 @@ function callback_batch_finished($success, $results, $operations) {
*/
function hook_ajax_render_alter(array &$data) {
// Inject any new status messages into the content area.
$status_messages = array('#theme' => 'status_messages');
$command = new \Drupal\Core\Ajax\PrependCommand('#block-system-main .content', drupal_render($status_messages));
$status_messages = array('#type' => 'status_messages');
$command = new \Drupal\Core\Ajax\PrependCommand('#block-system-main .content', \Drupal::service('renderer')->renderRoot($status_messages));
$data[] = $command->render();
}
......
......@@ -10,6 +10,7 @@
use Drupal\Core\Ajax\UpdateBuildIdCommand;
use Drupal\Core\Form\FormState;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\system\FileAjaxForm;
use Drupal\Core\Form\FormBuilderInterface;
use Psr\Log\LoggerInterface;
......@@ -37,18 +38,27 @@ class FormAjaxController implements ContainerInjectionInterface {
*/
protected $formBuilder;
/**
* The renderer.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* Constructs a FormAjaxController object.
*
* @param \Psr\Log\LoggerInterface $logger
* A logger instance.
*
* @param \Drupal\Core\Form\FormBuilderInterface $form_builder
* The form builder.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer.
*/
public function __construct(LoggerInterface $logger, FormBuilderInterface $form_builder) {
public function __construct(LoggerInterface $logger, FormBuilderInterface $form_builder, RendererInterface $renderer) {
$this->logger = $logger;
$this->formBuilder = $form_builder;
$this->renderer = $renderer;
}
/**
......@@ -57,7 +67,8 @@ public function __construct(LoggerInterface $logger, FormBuilderInterface $form_
public static function create(ContainerInterface $container) {
return new static(
$container->get('logger.factory')->get('ajax'),
$container->get('form_builder')
$container->get('form_builder'),
$container->get('renderer')
);
}
......
<?php
/**
* @file
* Contains \Drupal\system\Plugin\Block\SystemMessagesBlock.
*/
namespace Drupal\system\Plugin\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Block\MessagesBlockPluginInterface;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Form\FormStateInterface;