Commit 64f9bb95 authored by alexpott's avatar alexpott

Issue #2796581 by tim.plunkett, swentel, amateescu: Fields must store their...

Issue #2796581 by tim.plunkett, swentel, amateescu: Fields must store their region in entity displays
parent b9dafa09
......@@ -64,6 +64,9 @@ core.entity_view_display.*.*.*:
weight:
type: integer
label: 'Weight'
region:
type: string
label: 'Region'
label:
type: string
label: 'Label setting machine name'
......@@ -115,6 +118,9 @@ core.entity_form_display.*.*.*:
weight:
type: integer
label: 'Weight'
region:
type: string
label: 'Region'
settings:
type: field.widget.settings.[%parent.type]
label: 'Settings'
......
......@@ -154,6 +154,7 @@ public function __construct(array $values, $entity_type) {
protected function init() {
// Only populate defaults for "official" view modes and form modes.
if ($this->mode !== static::CUSTOM_MODE) {
$default_region = $this->getDefaultRegion();
// Fill in defaults for extra fields.
$context = $this->displayContext == 'view' ? 'display' : $this->displayContext;
$extra_fields = \Drupal::entityManager()->getExtraFields($this->targetEntityType, $this->bundle);
......@@ -163,6 +164,8 @@ protected function init() {
// Extra fields are visible by default unless they explicitly say so.
if (!isset($definition['visible']) || $definition['visible'] == TRUE) {
$this->content[$name] = array(
'type' => 'visible',
'region' => $default_region,
'weight' => $definition['weight']
);
}
......@@ -178,10 +181,13 @@ protected function init() {
if (!$definition->isDisplayConfigurable($this->displayContext) || (!isset($this->content[$name]) && !isset($this->hidden[$name]))) {
$options = $definition->getDisplayOptions($this->displayContext);
if (!empty($options['type']) && $options['type'] == 'hidden') {
// Check if either 'type' or 'region' is set to hidden.
// @todo Remove handling of 'type' in https://www.drupal.org/node/2799641.
if ((!empty($options['type']) && $options['type'] === 'hidden') || (!empty($options['region']) && $options['region'] === 'hidden')) {
$this->hidden[$name] = TRUE;
}
elseif ($options) {
$options += ['region' => $default_region];
$this->content[$name] = $this->pluginManager->prepareConfiguration($definition->getType(), $options);
}
// Note: (base) fields that do not specify display options are not
......@@ -334,6 +340,12 @@ public function setComponent($name, array $options = array()) {
// Ensure we always have an empty settings and array.
$options += ['settings' => [], 'third_party_settings' => []];
// Ensure that a region is set.
// @todo Make 'region' required in https://www.drupal.org/node/2799641.
if (!isset($options['region'])) {
$options['region'] = (isset($options['type']) && $options['type'] === 'hidden') ? 'hidden' : $this->getDefaultRegion();
}
$this->content[$name] = $options;
unset($this->hidden[$name]);
unset($this->plugins[$name]);
......@@ -504,6 +516,16 @@ protected function getPluginRemovedDependencies(array $plugin_dependencies, arra
return $intersect;
}
/**
* Gets the default region.
*
* @return string
* The default region for this display.
*/
protected function getDefaultRegion() {
return 'content';
}
/**
* {@inheritdoc}
*/
......
......@@ -414,7 +414,8 @@ public function setDisplayOptions($display_context, array $options) {
public function setDisplayConfigurable($display_context, $configurable) {
// If no explicit display options have been specified, default to 'hidden'.
if (empty($this->definition['display'][$display_context])) {
$this->definition['display'][$display_context]['options'] = array('type' => 'hidden');
// @todo Remove handling of 'type' in https://www.drupal.org/node/2799641.
$this->definition['display'][$display_context]['options'] = array('type' => 'hidden', 'region' => 'hidden');
}
$this->definition['display'][$display_context]['configurable'] = $configurable;
return $this;
......
......@@ -11,20 +11,26 @@ content:
checked:
type: timestamp_ago
weight: 1
region: content
settings: { }
third_party_settings: { }
label: inline
description:
weight: 3
region: content
feed_icon:
weight: 5
region: content
image:
weight: 2
region: content
items:
weight: 0
region: content
link:
type: uri_link
weight: 4
region: content
settings: { }
third_party_settings: { }
label: inline
......
......@@ -12,8 +12,10 @@ mode: summary
content:
items:
weight: 0
region: content
more_link:
weight: 1
region: content
hidden:
checked: true
description: true
......
......@@ -12,6 +12,7 @@ mode: summary
content:
timestamp:
weight: 0
region: content
hidden:
author: true
description: true
......
......@@ -14,6 +14,7 @@ content:
body:
type: text_textarea_with_summary
weight: 26
region: content
settings:
rows: 9
summary_rows: 3
......@@ -22,6 +23,7 @@ content:
created:
type: datetime_timestamp
weight: 10
region: content
settings: { }
third_party_settings: { }
promote:
......@@ -29,16 +31,19 @@ content:
settings:
display_label: true
weight: 15
region: content
third_party_settings: { }
sticky:
type: boolean_checkbox
settings:
display_label: true
weight: 16
region: content
third_party_settings: { }
title:
type: string_textfield
weight: -5
region: content
settings:
size: 60
placeholder: ''
......@@ -46,6 +51,7 @@ content:
uid:
type: entity_reference_autocomplete
weight: 5
region: content
settings:
match_operator: CONTAINS
size: 60
......
......@@ -16,8 +16,10 @@ content:
label: hidden
type: text_default
weight: 100
region: content
settings: { }
third_party_settings: { }
links:
weight: 101
region: content
hidden: { }
......@@ -17,9 +17,11 @@ content:
label: hidden
type: text_summary_or_trimmed
weight: 100
region: content
settings:
trim_length: 600
third_party_settings: { }
links:
weight: 101
region: content
hidden: { }
......@@ -306,7 +306,8 @@ public function isDisplayConfigurable($context) {
*/
public function getDisplayOptions($display_context) {
// Hide configurable fields by default.
return array('type' => 'hidden');
// @todo Remove handling of 'type' in https://www.drupal.org/node/2799641.
return array('type' => 'hidden', 'region' => 'hidden');
}
/**
......
......@@ -32,6 +32,7 @@ public function testEntityDisplaySettings() {
'type' => 'text_trimmed',
'settings' => array('trim_length' => 600),
'third_party_settings' => array(),
'region' => 'content',
);
// Can we load any entity display.
......
......@@ -33,6 +33,7 @@ public function testWidgetSettings() {
$expected = array('weight' => 1, 'type' => 'text_textfield');
$expected['settings'] = array('size' => 60, 'placeholder' => '');
$expected['third_party_settings'] = array();
$expected['region'] = 'content';
$this->assertIdentical($expected, $component, 'Text field settings are correct.');
// Integer field.
......
......@@ -128,9 +128,14 @@
var refreshRows = {};
refreshRows[rowHandler.name] = $trigger.get(0);
// Handle region change.
// Handle region or type change.
var region = rowHandler.getRegion();
if (region !== rowHandler.region) {
// @todo Remove handling of 'type' in https://www.drupal.org/node/2799641.
var typeRegion = rowHandler.getType();
if (region !== rowHandler.region || typeRegion !== rowHandler.region) {
if (region === rowHandler.region) {
region = typeRegion;
}
// Remove parenting.
$row.find('select.js-field-parent').val('');
// Let the row handler deal with the region change.
......@@ -270,6 +275,10 @@
this.$pluginSelect = $(row).find('select.field-plugin-type');
this.$pluginSelect.on('change', Drupal.fieldUIOverview.onChange);
// Attach change listener to the 'region' select.
this.$regionSelect = $(row).find('select.field-region');
this.$regionSelect.on('change', Drupal.fieldUIOverview.onChange);
return this;
};
......@@ -282,6 +291,16 @@
* Either 'hidden' or 'content'.
*/
getRegion: function () {
return this.$regionSelect.val();
},
/**
* Returns the region corresponding to the current form values of the row.
*
* @return {string}
* Either 'hidden' or 'content'.
*/
getType: function () {
return (this.$pluginSelect.val() === 'hidden') ? 'hidden' : 'content';
},
......@@ -305,14 +324,17 @@
* {@link Drupal.fieldUIOverview.AJAXRefreshRows}.
*/
regionChange: function (region) {
// Replace dashes with underscores.
region = region.replace(/-/g, '_');
// Set the region of the select list.
this.$regionSelect.val(region);
// When triggered by a row drag, the 'format' select needs to be adjusted
// to the new region.
var currentValue = this.$pluginSelect.val();
var value;
// @TODO Check if this couldn't just be like
// if (region !== 'hidden') {
if (region === 'content') {
if (region !== 'hidden') {
if (currentValue === 'hidden') {
// Restore the formatter back to the default formatter. Pseudo-fields
// do not have default formatters, we just return to 'visible' for
......
......@@ -4,6 +4,7 @@
use Drupal\Component\Plugin\Factory\DefaultFactory;
use Drupal\Component\Plugin\PluginManagerBase;
use Drupal\Core\Entity\Display\EntityDisplayInterface;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityWithPluginCollectionInterface;
......@@ -172,6 +173,13 @@ public function form(array $form, FormStateInterface $form_state) {
'subgroup' => 'field-parent',
'source' => 'field-name',
),
array(
'action' => 'match',
'relationship' => 'parent',
'group' => 'field-region',
'subgroup' => 'field-region',
'source' => 'field-name',
),
),
);
......@@ -309,6 +317,15 @@ protected function buildFieldRow(FieldDefinitionInterface $field_definition, arr
'#attributes' => array('class' => array('field-name')),
),
),
'region' => array(
'#type' => 'select',
'#title' => $this->t('Region for @title', array('@title' => $label)),
'#title_display' => 'invisible',
'#options' => $this->getRegionOptions(),
'#empty_value' => 'hidden',
'#default_value' => isset($display_options['region']) ? $display_options['region'] : 'hidden',
'#attributes' => array('class' => array('field-region')),
),
);
$field_row['plugin'] = array(
......@@ -474,6 +491,15 @@ protected function buildExtraFieldRow($field_id, $extra_field) {
'#attributes' => array('class' => array('field-name')),
),
),
'region' => array(
'#type' => 'select',
'#title' => $this->t('Region for @title', array('@title' => $extra_field['label'])),
'#title_display' => 'invisible',
'#options' => $this->getRegionOptions(),
'#empty_value' => 'hidden',
'#default_value' => $display_options ? $display_options['region'] : 'hidden',
'#attributes' => array('class' => array('field-region')),
),
'plugin' => array(
'type' => array(
'#type' => 'select',
......@@ -548,44 +574,119 @@ protected function copyFormValuesToEntity(EntityInterface $entity, array $form,
// Collect data for 'regular' fields.
foreach ($form['#fields'] as $field_name) {
$values = $form_values['fields'][$field_name];
$this->processFieldUpdates($field_name, $form_values['fields'][$field_name], $entity, $form_state);
}
// Collect data for 'extra' fields.
foreach ($form['#extra'] as $name) {
$this->processFieldUpdates($name, $form_values['fields'][$name], $entity, $form_state);
}
if ($values['type'] == 'hidden') {
$entity->removeComponent($field_name);
$form_state->setTemporaryValue('entity_display_components_updated', TRUE);
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
$form_state->setTemporaryValue('entity_display_components_updated', NULL);
return parent::save($form, $form_state);
}
/**
* Processes updates to the components for a given field.
*
* @param string $field_name
* The field name being processed.
* @param array $values
* The submitted form values.
* @param \Drupal\Core\Entity\Display\EntityDisplayInterface $entity
* The entity being updated.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
protected function processFieldUpdates($field_name, array $values, EntityDisplayInterface $entity, FormStateInterface $form_state) {
// If the component is not found, it is initially hidden.
$options = $entity->getComponent($field_name) ?: ['type' => 'hidden', 'region' => 'hidden'];
$remove_component = $options['region'] === 'hidden';
if ($form_state->getTemporaryValue('entity_display_components_updated')) {
// Since the component has already been updated, replace $values with the
// relevant parts of $options.
$values = array_intersect_key($options, $values) + $values;
}
// @todo In https://www.drupal.org/node/2799641, remove this else statement.
else {
$remove_component = $this->determineComponentAction($options, $values);
}
if ($remove_component) {
$entity->removeComponent($field_name);
}
else {
// Update field settings only if the submit handler told us to.
if ($form_state->get('plugin_settings_update') === $field_name) {
// Only store settings actually used by the selected plugin.
$default_settings = $this->pluginManager->getDefaultSettings($options['type']);
$options['settings'] = isset($values['settings_edit_form']['settings']) ? array_intersect_key($values['settings_edit_form']['settings'], $default_settings) : [];
$options['third_party_settings'] = isset($values['settings_edit_form']['third_party_settings']) ? $values['settings_edit_form']['third_party_settings'] : [];
$form_state->set('plugin_settings_update', NULL);
}
else {
$options = $entity->getComponent($field_name);
// Update field settings only if the submit handler told us to.
if ($form_state->get('plugin_settings_update') === $field_name) {
// Only store settings actually used by the selected plugin.
$default_settings = $this->pluginManager->getDefaultSettings($options['type']);
$options['settings'] = isset($values['settings_edit_form']['settings']) ? array_intersect_key($values['settings_edit_form']['settings'], $default_settings) : [];
$options['third_party_settings'] = isset($values['settings_edit_form']['third_party_settings']) ? $values['settings_edit_form']['third_party_settings'] : [];
$form_state->set('plugin_settings_update', NULL);
}
if (isset($values['type'])) {
$options['type'] = $values['type'];
$options['weight'] = $values['weight'];
// Only formatters have configurable label visibility.
if (isset($values['label'])) {
$options['label'] = $values['label'];
}
$entity->setComponent($field_name, $options);
}
$options['weight'] = $values['weight'];
if (isset($values['region'])) {
$options['region'] = $values['region'];
}
// Only formatters have configurable label visibility.
if (isset($values['label'])) {
$options['label'] = $values['label'];
}
$entity->setComponent($field_name, $options);
}
}
// Collect data for 'extra' fields.
foreach ($form['#extra'] as $name) {
if ($form_values['fields'][$name]['type'] == 'hidden') {
$entity->removeComponent($name);
/**
* Determines whether a component should be updated or removed.
*
* @todo Remove handling of 'type' in https://www.drupal.org/node/2799641.
*
* @param array $old_values
* An array of the old values for a given component.
* @param array $new_values
* An array of the new values for a given component.
*
* @return bool
* TRUE if the component should be removed, FALSE if it should be updated.
*/
protected function determineComponentAction(array &$old_values, array &$new_values) {
$has_type_change = $new_values['type'] !== $old_values['type'];
$has_region_change = $new_values['region'] !== $old_values['region'];
// If the type and region both changed or neither changed, the action will
// be the same. Base the decision on whether the region is hidden.
if ($has_type_change === $has_region_change) {
$remove_component = $new_values['region'] === 'hidden';
}
else {
if ($has_region_change) {
// If only the region changed, remove the component if it is now hidden.
$remove_component = $new_values['region'] === 'hidden';
// If the region and type mismatch, remove the invalid type.
if ($new_values['region'] !== 'hidden' && $new_values['type'] === 'hidden') {
unset($new_values['type'], $old_values['type']);
}
}
else {
$entity->setComponent($name, array(
'weight' => $form_values['fields'][$name]['weight'],
));
// If only the type changed, remove the component if it is now hidden.
$remove_component = $new_values['type'] === 'hidden';
// If the region and type mismatch, remove the invalid region.
if ($new_values['region'] === 'hidden' && $new_values['type'] !== 'hidden') {
unset($new_values['region'], $old_values['region']);
}
}
}
return $remove_component;
}
/**
......@@ -813,7 +914,7 @@ public function getRowRegion($row) {
switch ($row['#row_type']) {
case 'field':
case 'extra_field':
return ($row['plugin']['type']['#value'] == 'hidden' ? 'hidden' : 'content');
return $row['region']['#value'] ?: 'hidden';
}
}
......
......@@ -94,6 +94,7 @@ protected function getTableHeader() {
$this->t('Field'),
$this->t('Weight'),
$this->t('Parent'),
$this->t('Region'),
array('data' => $this->t('Widget'), 'colspan' => 3),
);
}
......
......@@ -127,6 +127,7 @@ protected function getTableHeader() {
$this->t('Field'),
$this->t('Weight'),
$this->t('Parent'),
$this->t('Region'),
$this->t('Label'),
array('data' => $this->t('Format'), 'colspan' => 3),
);
......
......@@ -3,6 +3,7 @@
namespace Drupal\field_ui\Tests;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Language\LanguageInterface;
......@@ -98,6 +99,19 @@ function testFormatterUI() {
);
$this->assertEqual($options, $expected_options, 'The expected formatter ordering is respected.');
// Ensure that fields can be hidden directly by changing the region.
$this->drupalGet($manage_display);
$this->assertFieldByName('fields[field_test][region]', 'content');
$edit = ['fields[field_test][region]' => 'hidden'];
$this->drupalPostForm($manage_display, $edit, t('Save'));
$this->assertFieldByName('fields[field_test][region]', 'hidden');
$display = EntityViewDisplay::load("node.{$this->type}.default");
$this->assertNull($display->getComponent('field_test'));
// Restore the field to the content region.
$edit = ['fields[field_test][region]' => 'content'];
$this->drupalPostForm($manage_display, $edit, t('Save'));
// Change the formatter and check that the summary is updated.
$edit = array('fields[field_test][type]' => 'field_test_multiple', 'refresh_rows' => 'field_test');
$this->drupalPostAjaxForm(NULL, $edit, array('op' => t('Refresh')));
......@@ -284,6 +298,14 @@ public function testWidgetUI() {
// Checks if the select elements contain the specified options.
$this->assertFieldSelectOptions('fields[field_test][type]', array('test_field_widget', 'test_field_widget_multiple', 'hidden'));
$this->assertFieldSelectOptions('fields[field_onewidgetfield][type]', array('test_field_widget', 'hidden'));
// Ensure that fields can be hidden directly by changing the region.
$this->assertFieldByName('fields[field_test][region]', 'content');
$edit = ['fields[field_test][region]' => 'hidden'];
$this->drupalPostForm(NULL, $edit, t('Save'));
$this->assertFieldByName('fields[field_test][region]', 'hidden');
$display = EntityFormDisplay::load("node.{$this->type}.default");
$this->assertNull($display->getComponent('field_test'));
}
/**
......
<?php
namespace Drupal\Tests\field_ui\FunctionalJavascript;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
/**
* Tests the UI for entity displays.
*
* @group field_ui
*/
class EntityDisplayTest extends JavascriptTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['field_ui', 'entity_test'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$entity = EntityTest::create([
'name' => 'The name for this entity',
'field_test_text' => [[
'value' => 'The field test text value',
]],
]);
$entity->save();
$this->drupalLogin($this->drupalCreateUser([
'access administration pages',
'view test entity',
'administer entity_test content',
'administer entity_test fields',
'administer entity_test display',
'administer entity_test form display',
'view the administration theme',
]));
}
/**
* Tests the use of regions for entity form displays.
*/
public function testEntityForm() {
$this->drupalGet('entity_test/manage/1/edit');
$this->assertSession()->fieldExists('field_test_text[0][value]');
$this->drupalGet('entity_test/structure/entity_test/form-display');
$this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'content')->isSelected());
$this->assertFalse($this->assertSession()->optionExists('fields[field_test_text][type]', 'hidden')->isSelected());
$this->getSession()->getPage()->selectFieldOption('fields[field_test_text][region]', 'hidden');
$this->assertSession()->assertWaitOnAjaxRequest();
$this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'hidden')->isSelected());
$this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][type]', 'hidden')->isSelected());
$this->submitForm([], 'Save');
$this->assertSession()->pageTextContains('Your settings have been saved.');
$this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'hidden')->isSelected());
$this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][type]', 'hidden')->isSelected());
$this->drupalGet('entity_test/manage/1/edit');
$this->assertSession()->fieldNotExists('field_test_text[0][value]');
}
/**
* Tests the use of regions for entity view displays.
*/
public function testEntityView() {
$this->drupalGet('entity_test/1');
$this->assertSession()->elementNotExists('css', '.field--name-field-test-text');
$this->drupalGet('entity_test/structure/entity_test/display');
$this->assertSession()->elementExists('css', '.region-content-message.region-empty');
$this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'hidden')->isSelected());
$this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][type]', 'hidden')->isSelected());
$this->getSession()->getPage()->selectFieldOption('fields[field_test_text][region]', 'content');
$this->assertSession()->assertWaitOnAjaxRequest();
$this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'content')->isSelected());
$this->assertFalse($this->assertSession()->optionExists('fields[field_test_text][type]', 'hidden')->isSelected());
$this->submitForm([], 'Save');
$this->assertSession()->pageTextContains('Your settings have been saved.');
$this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'content')->isSelected());
$this->assertFalse($this->assertSession()->optionExists('fields[field_test_text][type]', 'hidden')->isSelected());
$this->drupalGet('entity_test/1');
$this->assertSession()->elementExists('css', '.field--name-field-test-text');
}
}
......@@ -53,15 +53,15 @@ public function testEntityDisplayCRUD() {
// Check that providing no 'weight' results in the highest current weight
// being assigned. The 'name' field's formatter has weight -5, therefore
// these follow.
$expected['component_1'] = array('weight' => -4, 'settings' => array(), 'third_party_settings' => array());
$expected['component_2'] = array('weight' => -3, 'settings' => array(), 'third_party_settings' => array());
$expected['component_1'] = array('weight' => -4, 'settings' => array(), 'third_party_settings' => array(), 'region' => 'content');
$expected['component_2'] = array('weight' => -3, 'settings' => array(), 'third_party_settings' => array(), 'region' => 'content');