Commit 962eddec authored by catch's avatar catch

Issue #2796581 by tim.plunkett, jibran, catch, swentel, alexpott, cilefen,...

Issue #2796581 by tim.plunkett, jibran, catch, swentel, alexpott, cilefen, xjm, amateescu: Fields must store their region in entity displays
parent b366320e
......@@ -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'
......
......@@ -514,7 +514,7 @@ function entity_get_display($entity_type, $bundle, $view_mode) {
* 'weight' => 1,
* ))
* ->setComponent('field_image', array(
* 'type' => 'hidden',
* 'region' => 'hidden',
* ))
* ->save();
* @endcode
......
......@@ -1171,7 +1171,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
$fields[$entity_type->getKey('langcode')] = BaseFieldDefinition::create('language')
->setLabel(new TranslatableMarkup('Language'))
->setDisplayOptions('view', [
'type' => 'hidden',
'region' => 'hidden',
])
->setDisplayOptions('form', [
'type' => 'language_select',
......
......@@ -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);
......@@ -170,6 +171,10 @@ protected function init() {
$this->hidden[$name] = TRUE;
}
}
// Ensure extra fields have a 'region'.
if (isset($this->content[$name])) {
$this->content[$name] += ['region' => $default_region];
}
}
// Fill in defaults for fields.
......@@ -178,10 +183,17 @@ 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') {
// @todo Remove handling of 'type' in https://www.drupal.org/node/2799641.
if (!isset($options['region']) && !empty($options['type']) && $options['type'] === 'hidden') {
$options['region'] = 'hidden';
@trigger_error("Specifying 'type' => 'hidden' is deprecated, use 'region' => 'hidden' instead.", E_USER_DEPRECATED);
}
if (!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
......@@ -239,11 +251,39 @@ public function id() {
* {@inheritdoc}
*/
public function preSave(EntityStorageInterface $storage, $update = TRUE) {
// Ensure that a region is set on each component.
foreach ($this->getComponents() as $name => $component) {
$this->handleHiddenType($name, $component);
// Ensure that a region is set.
if (isset($this->content[$name]) && !isset($component['region'])) {
// Directly set the component to bypass other changes in setComponent().
$this->content[$name]['region'] = $this->getDefaultRegion();
}
}
ksort($this->content);
ksort($this->hidden);
parent::preSave($storage, $update);
}
/**
* Handles a component type of 'hidden'.
*
* @deprecated This method exists only for backwards compatibility.
*
* @todo Remove this in https://www.drupal.org/node/2799641.
*
* @param string $name
* The name of the component.
* @param array $component
* The component array.
*/
protected function handleHiddenType($name, array $component) {
if (!isset($component['region']) && isset($component['type']) && $component['type'] === 'hidden') {
$this->removeComponent($name);
}
}
/**
* {@inheritdoc}
*/
......@@ -504,6 +544,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}
*/
......
......@@ -1638,7 +1638,7 @@ function hook_entity_form_display_alter(\Drupal\Core\Entity\Display\EntityFormDi
// Hide the 'user_picture' field from the register form.
if ($context['entity_type'] == 'user' && $context['form_mode'] == 'register') {
$form_display->setComponent('user_picture', array(
'type' => 'hidden',
'region' => 'hidden',
));
}
}
......
......@@ -414,7 +414,7 @@ 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');
$this->definition['display'][$display_context]['options'] = array('region' => 'hidden');
}
$this->definition['display'][$display_context]['configurable'] = $configurable;
return $this;
......
......@@ -137,10 +137,13 @@ public function isDisplayConfigurable($display_context);
* - label: (string) Position of the field label. The default 'field' theme
* implementation supports the values 'inline', 'above' and 'hidden'.
* Defaults to 'above'. Only applies to 'view' context.
* - region: (string) The region the field is in, or 'hidden'. If not
* specified, the default region will be used.
* - type: (string) The plugin (widget or formatter depending on
* $display_context) to use, or 'hidden'. If not specified or if the
* requested plugin is unknown, the 'default_widget' / 'default_formatter'
* for the field type will be used.
* $display_context) to use. If not specified or if the requested plugin
* is unknown, the 'default_widget' / 'default_formatter' for the field
* type will be used. Previously 'hidden' was a valid value, it is now
* deprecated in favor of specifying 'region' => 'hidden'.
* - settings: (array) Settings for the plugin specified above. The default
* settings for the plugin will be used for settings left unspecified.
* - third_party_settings: (array) Settings provided by other extensions
......
......@@ -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
......
......@@ -76,7 +76,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
->setLabel(t('Link'))
->setDescription(t('The link of the feed item.'))
->setDisplayOptions('view', array(
'type' => 'hidden',
'region' => 'hidden',
))
->setDisplayConfigurable('view', 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: { }
......@@ -299,7 +299,7 @@ public function entityBaseFieldInfo(EntityTypeInterface $entity_type) {
->setSetting('target_type', 'moderation_state')
->setDisplayOptions('view', [
'label' => 'hidden',
'type' => 'hidden',
'region' => 'hidden',
'weight' => -5,
])
->setDisplayOptions('form', [
......
......@@ -306,7 +306,7 @@ public function isDisplayConfigurable($context) {
*/
public function getDisplayOptions($display_context) {
// Hide configurable fields by default.
return array('type' => 'hidden');
return array('region' => 'hidden');
}
/**
......
......@@ -375,7 +375,6 @@ public function testAvailableFormatters() {
'entity_reference_entity_id',
'entity_reference_rss_category',
'entity_reference_entity_view',
'hidden',
));
// Test if User Reference Field has the correct formatters.
......@@ -386,7 +385,6 @@ public function testAvailableFormatters() {
'entity_reference_entity_id',
'entity_reference_entity_view',
'entity_reference_label',
'hidden',
));
// Test if Node Entity Reference Field has the correct formatters.
......@@ -395,7 +393,6 @@ public function testAvailableFormatters() {
'entity_reference_label',
'entity_reference_entity_id',
'entity_reference_entity_view',
'hidden',
));
// Test if Date Format Reference Field has the correct formatters.
......@@ -404,7 +401,6 @@ public function testAvailableFormatters() {
$this->assertFieldSelectOptions('fields[field_' . $date_format_field_name . '][type]', array(
'entity_reference_label',
'entity_reference_entity_id',
'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.
......
......@@ -265,11 +265,16 @@
this.name = data.name;
this.region = data.region;
this.tableDrag = data.tableDrag;
this.defaultPlugin = data.defaultPlugin;
// Attach change listener to the 'plugin type' select.
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,7 +287,7 @@
* Either 'hidden' or 'content'.
*/
getRegion: function () {
return (this.$pluginSelect.val() === 'hidden') ? 'hidden' : 'content';
return this.$regionSelect.val();
},
/**
......@@ -305,24 +310,16 @@
* {@link Drupal.fieldUIOverview.AJAXRefreshRows}.
*/
regionChange: function (region) {
// Replace dashes with underscores.
region = region.replace(/-/g, '_');
// 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 (currentValue === 'hidden') {
// Restore the formatter back to the default formatter. Pseudo-fields
// do not have default formatters, we just return to 'visible' for
// those.
value = (typeof this.defaultPlugin !== 'undefined') ? this.defaultPlugin : this.$pluginSelect.find('option').val();
}
}
else {
value = 'hidden';
}
// Set the region of the select list.
this.$regionSelect.val(region);
// Restore the formatter back to the default formatter. Pseudo-fields
// do not have default formatters, we just return to 'visible' for
// those.
var value = (typeof this.defaultPlugin !== 'undefined') ? this.defaultPlugin : this.$pluginSelect.find('option').val();
if (typeof value !== 'undefined') {
this.$pluginSelect.val(value);
......
......@@ -172,6 +172,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 +316,14 @@ 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(),
'#default_value' => $display_options ? $display_options['region'] : 'hidden',
'#attributes' => array('class' => array('field-region')),
),
);
$field_row['plugin'] = array(
......@@ -316,7 +331,7 @@ protected function buildFieldRow(FieldDefinitionInterface $field_definition, arr
'#type' => 'select',
'#title' => $this->t('Plugin for @title', array('@title' => $label)),
'#title_display' => 'invisible',
'#options' => $this->getPluginOptions($field_definition),
'#options' => $this->getApplicablePluginOptions($field_definition),
'#default_value' => $display_options ? $display_options['type'] : 'hidden',
'#parents' => array('fields', $field_name, 'type'),
'#attributes' => array('class' => array('field-plugin-type')),
......@@ -474,6 +489,14 @@ 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(),
'#default_value' => $display_options ? $display_options['region'] : 'hidden',
'#attributes' => array('class' => array('field-region')),
),
'plugin' => array(
'type' => array(
'#type' => 'select',
......@@ -550,7 +573,7 @@ protected function copyFormValuesToEntity(EntityInterface $entity, array $form,
foreach ($form['#fields'] as $field_name) {
$values = $form_values['fields'][$field_name];
if ($values['type'] == 'hidden') {
if ($values['region'] == 'hidden') {
$entity->removeComponent($field_name);
}
else {
......@@ -567,6 +590,7 @@ protected function copyFormValuesToEntity(EntityInterface $entity, array $form,
$options['type'] = $values['type'];
$options['weight'] = $values['weight'];
$options['region'] = $values['region'];
// Only formatters have configurable label visibility.
if (isset($values['label'])) {
$options['label'] = $values['label'];
......@@ -577,12 +601,13 @@ protected function copyFormValuesToEntity(EntityInterface $entity, array $form,
// Collect data for 'extra' fields.
foreach ($form['#extra'] as $name) {
if ($form_values['fields'][$name]['type'] == 'hidden') {
if ($form_values['fields'][$name]['region'] == 'hidden') {
$entity->removeComponent($name);
}
else {
$entity->setComponent($name, array(
'weight' => $form_values['fields'][$name]['weight'],
'region' => $form_values['fields'][$name]['region'],
));
}
}
......@@ -751,20 +776,6 @@ protected function getApplicablePluginOptions(FieldDefinitionInterface $field_de
return $applicable_options;
}
/**
* Returns an array of widget or formatter options for a field.
*
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
* The field definition.
*
* @return array
* An array of widget or formatter options.
*/
protected function getPluginOptions(FieldDefinitionInterface $field_definition) {
$applicable_options = $this->getApplicablePluginOptions($field_definition);
return $applicable_options + array('hidden' => '- ' . $this->t('Hidden') . ' -');
}
/**
* Returns the ID of the default widget or formatter plugin for a field type.
*
......@@ -813,7 +824,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';
}
}
......@@ -826,7 +837,6 @@ public function getRowRegion($row) {
protected function getExtraFieldVisibilityOptions() {
return array(
'visible' => $this->t('Visible'),
'hidden' => '- ' . $this->t('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;
......@@ -94,12 +95,31 @@ function testFormatterUI() {
'field_test_multiple',
'field_test_with_prepare_view',
'field_test_applicable',
'hidden',
);
$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][type]' => 'field_test_default',
'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');
$edit = array(
'fields[field_test][type]' => 'field_test_multiple',
'fields[field_test][region]' => 'content',
'refresh_rows' => 'field_test'
);
$this->drupalPostAjaxForm(NULL, $edit, array('op' => t('Refresh')));
$format = 'field_test_multiple';
$default_settings = \Drupal::service('plugin.manager.field.formatter')->getDefaultSettings($format);
......@@ -147,7 +167,10 @@ function testFormatterUI() {
$this->assertFieldByName($fieldname, '');
// Test the empty setting formatter.
$edit = array('fields[field_test][type]' => 'field_empty_setting');
$edit = array(
'fields[field_test][type]' => 'field_empty_setting',
'fields[field_test][region]' => 'content',
);
$this->drupalPostForm(NULL, $edit, t('Save'));
$this->assertNoText('Default empty setting now has a value.');
$this->assertFieldById('edit-fields-field-test-settings-edit');
......@@ -159,7 +182,11 @@ function testFormatterUI() {
// Test the settings form behavior. An edit button should be present since
// there are third party settings to configure.
$edit = array('fields[field_test][type]' => 'field_no_settings', 'refresh_rows' => 'field_test');
$edit = array(
'fields[field_test][type]' => 'field_no_settings',
'fields[field_test][region]' => 'content',
'refresh_rows' => 'field_test',
);
$this->drupalPostAjaxForm(NULL, $edit, array('op' => t('Refresh')));
$this->assertFieldByName('field_test_settings_edit');
......@@ -225,12 +252,15 @@ public function testWidgetUI() {
$expected_options = array (
'test_field_widget',
'test_field_widget_multiple',
'hidden',
);
$this->assertEqual($options, $expected_options, 'The expected widget ordering is respected.');
// Change the widget and check that the summary is updated.
$edit = array('fields[field_test][type]' => 'test_field_widget_multiple', 'refresh_rows' => 'field_test');
$edit = array(
'fields[field_test][type]' => 'test_field_widget_multiple',
'fields[field_test][region]' => 'content',
'refresh_rows' => 'field_test',
);
$this->drupalPostAjaxForm(NULL, $edit, array('op' => t('Refresh')));
$widget_type = 'test_field_widget_multiple';
$default_settings = \Drupal::service('plugin.manager.field.widget')->getDefaultSettings($widget_type);
......@@ -282,8 +312,16 @@ public function testWidgetUI() {
$this->drupalGet($manage_display);
// 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'));
$this->assertFieldSelectOptions('fields[field_test][type]', array('test_field_widget', 'test_field_widget_multiple'));
$this->assertFieldSelectOptions('fields[field_onewidgetfield][type]', array('test_field_widget'));
// 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'));
}
/**
......@@ -321,6 +359,7 @@ function testViewModeCustom() {
// accordingly in 'rss' mode.
$edit = array(
'fields[field_test][type]' => 'field_test_with_prepare_view',
'fields[field_test][region]' => 'content',
);
$this->drupalPostForm('admin/structure/types/manage/' . $this->type . '/display', $edit, t('Save'));
$this->assertNodeViewText($node, 'rss', $output['field_test_with_prepare_view'], "The field is displayed as expected in view modes that use 'default' settings.");
......@@ -335,7 +374,7 @@ function testViewModeCustom() {
// Set the field to 'hidden' in the view mode, check that the field is
// hidden.
$edit = array(
'fields[field_test][type]' => 'hidden',
'fields[field_test][region]' => 'hidden',
);
$this->drupalPostForm('admin/structure/types/manage/' . $this->type . '/display/rss', $edit, t('Save'));
$this->assertNodeViewNoText($node, 'rss', $value, "The field is hidden in 'rss' mode.");
......@@ -378,7 +417,7 @@ function testNonInitializedFields() {