Unverified Commit 0a16b0f1 authored by alexpott's avatar alexpott
Browse files

Issue #3045171 by godotislate, rlmumford, bnjmnm, ccasals, bkosborne,...

Issue #3045171 by godotislate, rlmumford, bnjmnm, ccasals, bkosborne, johndevman, shimpy, Madhura BK, phjou, xaqrox, a3hill, tim.plunkett, gnuget, grahamC, kualee: Form blocks rendered inside layout builder break save
parent 386e98b8
......@@ -190,6 +190,13 @@ function layout_builder_post_update_update_permissions() {
}
}
/**
* Clear caches due to addition of service decorator for entity form controller.
*/
function layout_builder_post_update_override_entity_form_controller() {
// Empty post-update hook.
}
/**
* Set the layout builder field as non-translatable where possible.
*/
......
......@@ -44,3 +44,10 @@ services:
inline_block.usage:
class: Drupal\layout_builder\InlineBlockUsage
arguments: ['@database']
layout_builder.controller.entity_form:
# Override the entity form controller to handle the entity layout_builder
# operation.
decorates: controller.entity_form
class: Drupal\layout_builder\Controller\LayoutBuilderHtmlEntityFormController
public: false
arguments: ['@layout_builder.controller.entity_form.inner']
<?php
namespace Drupal\layout_builder\Controller;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Controller\FormController;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Core\Routing\RouteMatchInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Overrides the entity form controller service for layout builder operations.
*/
class LayoutBuilderHtmlEntityFormController {
use DependencySerializationTrait;
/**
* The entity form controller being decorated.
*
* @var \Drupal\Core\Controller\FormController
*/
protected $entityFormController;
/**
* Constructs a LayoutBuilderHtmlEntityFormController object.
*
* @param \Drupal\Core\Controller\FormController $entity_form_controller
* The entity form controller being decorated.
*/
public function __construct(FormController $entity_form_controller) {
$this->entityFormController = $entity_form_controller;
}
/**
* {@inheritdoc}
*/
public function getContentResult(Request $request, RouteMatchInterface $route_match) {
$form = $this->entityFormController->getContentResult($request, $route_match);
// If the form render element has a #layout_builder_element_keys property,
// first set the form element as a child of the root render array. Use the
// keys to get the layout builder element from the form render array and
// copy it to a separate child element of the root element to prevent any
// forms within the layout builder element from being nested.
if (isset($form['#layout_builder_element_keys'])) {
$build['form'] = &$form;
$layout_builder_element = &NestedArray::getValue($form, $form['#layout_builder_element_keys']);
$build['layout_builder'] = $layout_builder_element;
// Remove the layout builder element within the form.
$layout_builder_element = [];
return $build;
}
// If no #layout_builder_element_keys property, return form as is.
return $form;
}
}
......@@ -80,6 +80,7 @@ public function buildForm(array $form, FormStateInterface $form_state, SectionSt
$form['layout_builder'] = [
'#type' => 'layout_builder',
'#section_storage' => $section_storage,
'#process' => [[static::class, 'layoutBuilderElementGetKeys']],
];
$form['layout_builder_message'] = $this->buildMessage($section_storage->getContextValue('display'));
......@@ -87,6 +88,20 @@ public function buildForm(array $form, FormStateInterface $form_state, SectionSt
return parent::buildForm($form, $form_state);
}
/**
* Form element #process callback.
*
* Save the layout builder element array parents as a property on the top form
* element so that they can be used to access the element within the whole
* render array later.
*
* @see \Drupal\layout_builder\Controller\LayoutBuilderHtmlEntityFormController
*/
public static function layoutBuilderElementGetKeys(array $element, FormStateInterface $form_state, &$form) {
$form['#layout_builder_element_keys'] = $element['#array_parents'];
return $element;
}
/**
* Renders a message to display at the top of the layout builder.
*
......
......@@ -32,6 +32,21 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen
'#type' => 'layout_builder',
'#section_storage' => $this->getSectionStorage($form_state),
];
$element['#process'][] = [static::class, 'layoutBuilderElementGetKeys'];
return $element;
}
/**
* Form element #process callback.
*
* Save the layout builder element array parents as a property on the top form
* element so that they can be used to access the element within the whole
* render array later.
*
* @see \Drupal\layout_builder\Controller\LayoutBuilderHtmlEntityFormController
*/
public static function layoutBuilderElementGetKeys(array $element, FormStateInterface $form_state, &$form) {
$form['#layout_builder_element_keys'] = $element['#array_parents'];
return $element;
}
......
name: 'Layout Builder form block test'
type: module
description: 'Support module for testing layout building using blocks with forms.'
package: Testing
version: VERSION
core: 8.x
<?php
namespace Drupal\layout_builder_form_block_test\Plugin\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Form\FormInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a block containing a Form API form for use in Layout Builder tests.
*
* @Block(
* id = "layout_builder_form_block_test_form_api_form_block",
* admin_label = @Translation("Layout Builder form block test form api form block"),
* category = @Translation("Layout Builder form block test")
* )
*/
class TestFormApiFormBlock extends BlockBase implements ContainerFactoryPluginInterface, FormInterface {
/**
* The form builder service.
*
* @var \Drupal\Core\Form\FormBuilderInterface
*/
protected $formBuilder;
/**
* TestFormApiFormBlock constructor.
*
* @param array $configuration
* The plugin configuration, i.e. an array with configuration values keyed
* by configuration option name. The special key 'context' may be used to
* initialize the defined contexts by setting it to an array of context
* values keyed by context names.
* @param string $plugin_id
* The plugin ID for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Form\FormBuilderInterface $form_builder
* The form builder service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, FormBuilderInterface $form_builder) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->formBuilder = $form_builder;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('form_builder'));
}
/**
* {@inheritdoc}
*/
public function build() {
return $this->formBuilder->getForm($this);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'layout_builder_form_block_test_search_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form['keywords'] = [
'#title' => $this->t('Keywords'),
'#type' => 'textfield',
'#attributes' => [
'placeholder' => $this->t('Keywords'),
],
'#required' => TRUE,
'#title_display' => 'invisible',
'#weight' => 1,
];
$form['actions'] = [
'#type' => 'actions',
'submit' => [
'#name' => '',
'#type' => 'submit',
'#value' => $this->t('Search'),
],
'#weight' => 2,
];
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
}
}
<?php
namespace Drupal\layout_builder_form_block_test\Plugin\Block;
use Drupal\Core\Block\BlockBase;
/**
* Provides a block containing inline template with <form> tag.
*
* For use in Layout Builder tests.
*
* @Block(
* id = "layout_builder_form_block_test_inline_template_form_block",
* admin_label = @Translation("Layout Builder form block test inline template form block"),
* category = @Translation("Layout Builder form block test")
* )
*/
class TestInlineTemplateFormBlock extends BlockBase {
/**
* {@inheritdoc}
*/
public function build() {
$build['form'] = [
'#type' => 'inline_template',
'#template' => '<form method="POST"><label>{{ "Keywords"|t }}<input name="keyword" type="text" required /></label><input name="submit" type="submit" value="{{ "Submit"|t }}" /></form>',
];
return $build;
}
}
......@@ -203,3 +203,76 @@ display:
- 'user.node_grants:view'
- user.permissions
tags: { }
block_3:
display_plugin: block
id: block_3
display_title: 'Exposed form block'
position: 3
display_options:
display_extenders: { }
display_description: ''
filters:
status:
id: status
table: node_field_data
field: status
relationship: none
group_type: group
admin_label: ''
operator: '='
value: '1'
group: 1
exposed: true
expose:
operator_id: ''
label: 'Published status'
description: ''
use_operator: false
operator: status_op
operator_limit_selection: false
operator_list: { }
identifier: status
required: true
remember: false
multiple: false
remember_roles:
authenticated: authenticated
anonymous: '0'
administrator: '0'
author: '0'
editor: '0'
is_grouped: false
group_info:
label: ''
description: ''
identifier: ''
optional: true
widget: select
multiple: false
remember: false
default_group: All
default_group_multiple: { }
group_items: { }
plugin_id: boolean
entity_type: node
entity_field: status
defaults:
filters: false
filter_groups: false
use_ajax: false
title: false
filter_groups:
operator: AND
groups:
1: AND
use_ajax: true
title: 'Test Block View: Exposed form block'
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url
- 'user.node_grants:view'
- user.permissions
tags: { }
<?php
namespace Drupal\Tests\layout_builder\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Tests placing blocks containing forms in theLayout Builder UI.
*
* @group layout_builder
*/
class LayoutBuilderNestedFormUiTest extends WebDriverTestBase {
/**
* The form block labels used as text for links to add blocks.
*/
const FORM_BLOCK_LABELS = [
'Layout Builder form block test form api form block',
'Layout Builder form block test inline template form block',
'Test Block View: Exposed form block',
];
/**
* {@inheritdoc}
*/
public static $modules = [
'block',
'node',
'layout_builder',
'layout_builder_form_block_test',
'views',
'layout_builder_views_test',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->drupalPlaceBlock('local_tasks_block');
// Create a separate node to add a form block to, respectively.
// - Block with form api form will be added to first node layout.
// - Block with inline template with <form> tag added to second node layout.
// - Views block exposed form added to third node layout.
$this->createContentType([
'type' => 'bundle_with_section_field',
'name' => 'Bundle with section field',
]);
for ($i = 1; $i <= count(static::FORM_BLOCK_LABELS); $i++) {
$this->createNode([
'type' => 'bundle_with_section_field',
'title' => "Node $i title",
]);
}
}
/**
* Test blocks containing forms can be successfully saved editing defaults.
*/
public function testAddingFormBlocksToDefaults() {
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'administer node display',
]));
// From the manage display page, enable Layout Builder.
$field_ui_prefix = 'admin/structure/types/manage/bundle_with_section_field';
$this->drupalGet("$field_ui_prefix/display/default");
$this->drupalPostForm(NULL, ['layout[enabled]' => TRUE], 'Save');
$this->drupalPostForm(NULL, ['layout[allow_custom]' => TRUE], 'Save');
// Save the entity view display so that it can be reverted to later.
/** @var \Drupal\Core\Config\StorageInterface $active_config_storage */
$active_config_storage = $this->container->get('config.storage');
$original_display_config_data = $active_config_storage->read('core.entity_view_display.node.bundle_with_section_field.default');
/** @var \Drupal\Core\Config\Entity\ConfigEntityStorageInterface $entity_view_display_storage */
$entity_view_display_storage = $this->container->get('entity_type.manager')->getStorage('entity_view_display');
$entity_view_display = $entity_view_display_storage->load('node.bundle_with_section_field.default');
$expected_save_message = 'The layout has been saved.';
foreach (static::FORM_BLOCK_LABELS as $label) {
$this->addFormBlock($label, "$field_ui_prefix/display/default", $expected_save_message);
// Revert the entity view display back to remove the previously added form
// block.
$entity_view_display = $entity_view_display_storage
->updateFromStorageRecord($entity_view_display, $original_display_config_data);
$entity_view_display->save();
}
}
/**
* Test blocks containing forms can be successfully saved editing overrides.
*/
public function testAddingFormBlocksToOverrides() {
$this->drupalLogin($this->drupalCreateUser([
'configure any layout',
'administer node display',
]));
// From the manage display page, enable Layout Builder.
$field_ui_prefix = 'admin/structure/types/manage/bundle_with_section_field';
$this->drupalGet("$field_ui_prefix/display/default");
$this->drupalPostForm(NULL, ['layout[enabled]' => TRUE], 'Save');
$this->drupalPostForm(NULL, ['layout[allow_custom]' => TRUE], 'Save');
$expected_save_message = 'The layout override has been saved.';
$nid = 1;
foreach (static::FORM_BLOCK_LABELS as $label) {
$this->addFormBlock($label, "node/$nid", $expected_save_message);
$nid++;
}
}
/**
* Adds a form block specified by label layout and checks it can be saved.
*
* Need to test saving and resaving, because nested forms can cause issues
* on the second save.
*
* @param string $label
* The form block label that will be used to identify link to add block.
* @param string $path
* Root path of the entity (i.e. node/{NID) or the entity view display path.
* @param string $expected_save_message
* The message that should be displayed after successful layout save.
*/
protected function addFormBlock($label, $path, $expected_save_message) {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
// Go to edit the layout.
$this->drupalGet($path . '/layout');
// Add the form block.
$assert_session->linkExists('Add block');
$this->clickLink('Add block');
$assert_session->assertWaitOnAjaxRequest();
$assert_session->linkExists($label);
$this->clickLink($label);
$assert_session->assertWaitOnAjaxRequest();
$page->pressButton('Add block');
$assert_session->assertWaitOnAjaxRequest();
$assert_session->pageTextContains($label);
$assert_session->addressEquals($path . '/layout');
// Save the defaults.
$page->pressButton('Save layout');
$assert_session->pageTextContains($expected_save_message);
$assert_session->addressEquals($path);
// Go back to edit layout and try to re-save.
$this->drupalGet($path . '/layout');
$page->pressButton('Save layout');
$assert_session->pageTextContains($expected_save_message);
$assert_session->addressEquals($path);
}
}
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