Commit 97464a9c authored by alexpott's avatar alexpott

Issue #2648956 by lokapujya, dawehner, Lendude, sidharrell, jibran, catch,...

Issue #2648956 by lokapujya, dawehner, Lendude, sidharrell, jibran, catch, alexpott: Editing a view leaves old keys hanging, results in invalid schema
parent 032d127b
......@@ -81,6 +81,16 @@ public function testHandlerUI() {
$this->assertEqual($view->field['field_name_0']->options['type'], 'text_trimmed');
$this->assertEqual($view->field['field_name_0']->options['settings']['trim_length'], $random_number);
// Now change the formatter back to 'default' which doesn't have any
// settings. We want to ensure that the settings are empty then.
$edit['options[type]'] = 'text_default';
$this->drupalPostForm('admin/structure/views/nojs/handler/test_view_fieldapi/default/field/field_name_0', $edit, t('Apply'));
$this->drupalPostForm('admin/structure/views/view/test_view_fieldapi', [], t('Save'));
$view = Views::getView('test_view_fieldapi');
$view->initHandlers();
$this->assertEqual($view->field['field_name_0']->options['type'], 'text_default');
$this->assertEqual($view->field['field_name_0']->options['settings'], []);
// Ensure that the view depends on the field storage.
$dependencies = \Drupal::service('config.manager')->findConfigEntityDependents('config', [$this->fieldStorages[0]->getConfigDependencyName()]);
$this->assertTrue(isset($dependencies['views.view.test_view_fieldapi']), 'The view is dependent on the field storage.');
......
......@@ -816,4 +816,19 @@ public function submitTemporaryForm($form, FormStateInterface $form_state) {
$view->cacheSet();
}
/**
* Calculates options stored on the handler
*
* @param array $options
* The options stored in the handler
* @param array $form_state_options
* The newly submitted form state options.
*
* @return array
* The new options
*/
public function submitFormCalculateOptions(array $options, array $form_state_options) {
return $form_state_options + $options;
}
}
......@@ -472,7 +472,8 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) {
// Get the settings form.
$settings_form = array('#value' => array());
if ($formatter = $this->getFormatterInstance()) {
$format = isset($form_state->getUserInput()['options']['type']) ? $form_state->getUserInput()['options']['type'] : $this->options['type'];
if ($formatter = $this->getFormatterInstance($format)) {
$settings_form = $formatter->settingsForm($form, $form_state);
// Convert field UI selector states to work in the Views field form.
FormHelper::rewriteStatesSelector($settings_form, "fields[{$field->getName()}][settings_edit_form]", 'options');
......@@ -480,6 +481,21 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) {
$form['settings'] = $settings_form;
}
/**
* {@inheritdoc}
*/
public function submitFormCalculateOptions(array $options, array $form_state_options) {
// When we change the formatter type we don't want to keep any of the
// previous configured formatter settings, as there might be schema
// conflict.
unset($options['settings']);
$options = $form_state_options + $options;
if (!isset($options['settings'])) {
$options['settings'] = [];
}
return $options;
}
/**
* Provide options for multiple value fields.
*/
......@@ -937,13 +953,16 @@ protected function addSelfTokens(&$tokens, $item) {
* @return \Drupal\Core\Field\FormatterInterface|null
* The field formatter instance.
*/
protected function getFormatterInstance() {
$settings = $this->options['settings'] + $this->formatterPluginManager->getDefaultSettings($this->options['type']);
protected function getFormatterInstance($format = NULL) {
if (!isset($format)) {
$format = $this->options['type'];
}
$settings = $this->options['settings'] + $this->formatterPluginManager->getDefaultSettings($format);
$options = [
'field_definition' => $this->getFieldDefinition(),
'configuration' => [
'type' => $this->options['type'],
'type' => $format,
'settings' => $settings,
'label' => '',
'weight' => 0,
......
langcode: en
status: true
dependencies:
config:
- field.storage.node.body
module:
- node
- text
- user
id: test_field_body
label: test_field_body
module: views
description: ''
tag: ''
base_table: node_field_data
base_field: nid
core: 8.x
display:
default:
display_plugin: default
id: default
display_title: Master
position: 0
display_options:
access:
type: perm
options:
perm: 'access content'
cache:
type: tag
options: { }
query:
type: views_query
options:
disable_sql_rewrite: false
distinct: false
replica: false
query_comment: ''
query_tags: { }
exposed_form:
type: basic
options:
submit_button: Apply
reset_button: false
reset_button_label: Reset
exposed_sorts_label: 'Sort by'
expose_sort_order: true
sort_asc_label: Asc
sort_desc_label: Desc
pager:
type: mini
options:
items_per_page: 10
offset: 0
id: 0
total_pages: null
expose:
items_per_page: false
items_per_page_label: 'Items per page'
items_per_page_options: '5, 10, 25, 50'
items_per_page_options_all: false
items_per_page_options_all_label: '- All -'
offset: false
offset_label: Offset
tags:
previous: ‹‹
next: ››
style:
type: default
options:
grouping: { }
row_class: ''
default_row_class: true
uses_fields: false
row:
type: fields
options:
inline: { }
separator: ''
hide_empty: false
default_field_elements: true
fields:
body:
id: body
table: node__body
field: body
relationship: none
group_type: group
admin_label: 'Body field'
label: ''
exclude: false
alter:
alter_text: false
text: ''
make_link: false
path: ''
absolute: false
external: false
replace_spaces: false
path_case: none
trim_whitespace: false
alt: ''
rel: ''
link_class: ''
prefix: ''
suffix: ''
target: ''
nl2br: false
max_length: 0
word_boundary: true
ellipsis: true
more_link: false
more_link_text: ''
more_link_path: ''
strip_tags: false
trim: false
preserve_tags: ''
html: false
element_type: ''
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
click_sort_column: value
type: text_default
settings: { }
group_column: value
group_columns: { }
group_rows: true
delta_limit: 0
delta_offset: 0
delta_reversed: false
delta_first_last: false
multi_type: separator
separator: ', '
field_api_classes: false
plugin_id: field
filters:
status:
value: true
table: node_field_data
field: status
plugin_id: boolean
entity_type: node
entity_field: status
id: status
expose:
operator: ''
group: 1
sorts:
created:
id: created
table: node_field_data
field: created
order: DESC
entity_type: node
entity_field: created
plugin_id: date
relationship: none
group_type: group
admin_label: ''
exposed: false
expose:
label: ''
granularity: second
header: { }
footer: { }
empty: { }
relationships: { }
arguments: { }
display_extenders: { }
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- url.query_args
- 'user.node_grants:view'
- user.permissions
tags:
- 'config:field.storage.node.body'
<?php
namespace Drupal\Tests\views\FunctionalJavascript\Plugin\views\Handler;
use Drupal\config\Tests\SchemaCheckTestTrait;
use Drupal\field\Entity\FieldConfig;
use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
use Drupal\node\Entity\NodeType;
use Drupal\views\Tests\ViewTestData;
/**
* Tests the field field handler UI.
*
* @group views
*/
class FieldTest extends JavascriptTestBase {
use SchemaCheckTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['node', 'views', 'views_ui', 'views_test_config'];
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = ['test_field_body'];
/**
* The account.
*
* @var \Drupal\user\UserInterface
*/
protected $account;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
ViewTestData::createTestViews(get_class($this), ['views_test_config']);
// Disable automatic live preview to make the sequence of calls clearer.
\Drupal::configFactory()->getEditable('views.settings')->set('ui.always_live_preview', FALSE)->save();
$this->account = $this->drupalCreateUser(['administer views']);
$this->drupalLogin($this->account);
NodeType::create([
'type' => 'page',
])->save();
FieldConfig::create([
'entity_type' => 'node',
'field_name' => 'body',
'bundle' => 'page',
])->save();
}
public function testFormatterChanging() {
$web_assert = $this->assertSession();
$url = '/admin/structure/views/view/test_field_body';
$this->drupalGet($url);
$page = $this->getSession()->getPage();
$page->clickLink('Body field');
$web_assert->assertWaitOnAjaxRequest();
$page->fillField('options[type]', 'text_trimmed');
// Add a value to the trim_length setting.
$web_assert->assertWaitOnAjaxRequest();
$page->fillField('options[settings][trim_length]', '700');
$apply_button = $page->find('css', '.views-ui-dialog button.button--primary');
$this->assertTrue(!empty($apply_button));
$apply_button->press();
$web_assert->assertWaitOnAjaxRequest();
// Save the page.
$save_button = $page->find('css', '#edit-actions-submit');
$save_button->press();
// Set the body field back to 'default' and test that the trim_length
// settings are not in the config.
$this->drupalGet($url);
$page->clickLink('Body field');
$web_assert->assertWaitOnAjaxRequest();
$page->fillField('options[type]', 'text_default');
$web_assert->assertWaitOnAjaxRequest();
$apply_button = $page->find('css', '.views-ui-dialog button.button--primary');
$apply_button->press();
$web_assert->assertWaitOnAjaxRequest();
// Save the page.
$save_button = $page->find('css', '#edit-actions-submit');
$save_button->press();
$this->assertConfigSchemaByName('views.view.test_field_body');
}
}
......@@ -237,7 +237,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
// Add the incoming options to existing options because items using
// the extra form may not have everything in the form here.
$options = $form_state->getValue('options') + $handler->options;
$options = $handler->submitFormCalculateOptions($handler->options, $form_state->getValue('options', []));
// This unpacks only options that are in the definition, ensuring random
// extra stuff on the form is not sent through.
......
<?php
namespace Drupal\FunctionalJavascriptTests;
use Drupal\Tests\WebAssert;
/**
* Defines a class with methods for asserting presence of elements during tests.
*/
class JSWebAssert extends WebAssert {
/**
* Waits for AJAX request to be completed.
*
* @param int $timeout
* (Optional) Timeout in milliseconds, defaults to 10000.
* @param string $message
* (optional) A message for exception.
*
* @throws \RuntimeException
* When the request is not completed. If left blank, a default message will
* be displayed.
*/
public function assertWaitOnAjaxRequest($timeout = 10000, $message = 'Unable to complete AJAX request.') {
$result = $this->session->wait($timeout, '(typeof(jQuery)=="undefined" || (0 === jQuery.active && 0 === jQuery(\':animated\').length))');
if (!$result) {
throw new \RuntimeException($message);
}
}
}
......@@ -102,4 +102,11 @@ protected function assertJsCondition($condition, $timeout = 1000, $message = '')
$this->assertTrue($result, $message);
}
/**
* {@inheritdoc}
*/
public function assertSession($name = NULL) {
return new JSWebAssert($this->getSession($name), $this->baseUrl);
}
}
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