diff --git a/core/composer.json b/core/composer.json index 53ab8fa..862b5a3 100644 --- a/core/composer.json +++ b/core/composer.json @@ -94,6 +94,7 @@ "drupal/editor": "self.version", "drupal/entity_reference": "self.version", "drupal/field": "self.version", + "drupal/field_layout": "self.version", "drupal/field_ui": "self.version", "drupal/file": "self.version", "drupal/filter": "self.version", diff --git a/core/config/schema/core.entity.schema.yml b/core/config/schema/core.entity.schema.yml index bf0e12d..711ca0f 100644 --- a/core/config/schema/core.entity.schema.yml +++ b/core/config/schema/core.entity.schema.yml @@ -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' diff --git a/core/lib/Drupal/Core/Entity/EntityDisplayBase.php b/core/lib/Drupal/Core/Entity/EntityDisplayBase.php index 4de891a..0835aa0 100644 --- a/core/lib/Drupal/Core/Entity/EntityDisplayBase.php +++ b/core/lib/Drupal/Core/Entity/EntityDisplayBase.php @@ -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]); @@ -505,6 +517,16 @@ protected function getPluginRemovedDependencies(array $plugin_dependencies, arra } /** + * Gets the default region. + * + * @return string + * The default region for this display. + */ + protected function getDefaultRegion() { + return 'content'; + } + + /** * {@inheritdoc} */ public function __sleep() { diff --git a/core/lib/Drupal/Core/Field/BaseFieldDefinition.php b/core/lib/Drupal/Core/Field/BaseFieldDefinition.php index 594f31d..dd96fc9 100644 --- a/core/lib/Drupal/Core/Field/BaseFieldDefinition.php +++ b/core/lib/Drupal/Core/Field/BaseFieldDefinition.php @@ -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; diff --git a/core/modules/aggregator/config/install/core.entity_view_display.aggregator_feed.aggregator_feed.default.yml b/core/modules/aggregator/config/install/core.entity_view_display.aggregator_feed.aggregator_feed.default.yml index e3c23cf..e0232cf 100644 --- a/core/modules/aggregator/config/install/core.entity_view_display.aggregator_feed.aggregator_feed.default.yml +++ b/core/modules/aggregator/config/install/core.entity_view_display.aggregator_feed.aggregator_feed.default.yml @@ -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 diff --git a/core/modules/aggregator/config/install/core.entity_view_display.aggregator_feed.aggregator_feed.summary.yml b/core/modules/aggregator/config/install/core.entity_view_display.aggregator_feed.aggregator_feed.summary.yml index 40425f2..5e5e468 100644 --- a/core/modules/aggregator/config/install/core.entity_view_display.aggregator_feed.aggregator_feed.summary.yml +++ b/core/modules/aggregator/config/install/core.entity_view_display.aggregator_feed.aggregator_feed.summary.yml @@ -12,8 +12,10 @@ mode: summary content: items: weight: 0 + region: content more_link: weight: 1 + region: content hidden: checked: true description: true diff --git a/core/modules/aggregator/config/install/core.entity_view_display.aggregator_item.aggregator_item.summary.yml b/core/modules/aggregator/config/install/core.entity_view_display.aggregator_item.aggregator_item.summary.yml index 837bee0..8e29395 100644 --- a/core/modules/aggregator/config/install/core.entity_view_display.aggregator_item.aggregator_item.summary.yml +++ b/core/modules/aggregator/config/install/core.entity_view_display.aggregator_item.aggregator_item.summary.yml @@ -12,6 +12,7 @@ mode: summary content: timestamp: weight: 0 + region: content hidden: author: true description: true diff --git a/core/modules/book/config/install/core.entity_form_display.node.book.default.yml b/core/modules/book/config/install/core.entity_form_display.node.book.default.yml index 1ec4eb1..58aba45 100644 --- a/core/modules/book/config/install/core.entity_form_display.node.book.default.yml +++ b/core/modules/book/config/install/core.entity_form_display.node.book.default.yml @@ -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 diff --git a/core/modules/book/config/install/core.entity_view_display.node.book.default.yml b/core/modules/book/config/install/core.entity_view_display.node.book.default.yml index 729516e..d6ef64d 100644 --- a/core/modules/book/config/install/core.entity_view_display.node.book.default.yml +++ b/core/modules/book/config/install/core.entity_view_display.node.book.default.yml @@ -16,8 +16,10 @@ content: label: hidden type: text_default weight: 100 + region: content settings: { } third_party_settings: { } links: weight: 101 + region: content hidden: { } diff --git a/core/modules/book/config/install/core.entity_view_display.node.book.teaser.yml b/core/modules/book/config/install/core.entity_view_display.node.book.teaser.yml index fb22db6..77a62c3 100644 --- a/core/modules/book/config/install/core.entity_view_display.node.book.teaser.yml +++ b/core/modules/book/config/install/core.entity_view_display.node.book.teaser.yml @@ -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: { } diff --git a/core/modules/field/src/Entity/FieldConfig.php b/core/modules/field/src/Entity/FieldConfig.php index d7ccc30..1b79b6b 100644 --- a/core/modules/field/src/Entity/FieldConfig.php +++ b/core/modules/field/src/Entity/FieldConfig.php @@ -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'); } /** diff --git a/core/modules/field/tests/src/Kernel/Migrate/d6/MigrateFieldFormatterSettingsTest.php b/core/modules/field/tests/src/Kernel/Migrate/d6/MigrateFieldFormatterSettingsTest.php index 4c9cbf1..2ee3c77 100644 --- a/core/modules/field/tests/src/Kernel/Migrate/d6/MigrateFieldFormatterSettingsTest.php +++ b/core/modules/field/tests/src/Kernel/Migrate/d6/MigrateFieldFormatterSettingsTest.php @@ -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. diff --git a/core/modules/field/tests/src/Kernel/Migrate/d6/MigrateFieldWidgetSettingsTest.php b/core/modules/field/tests/src/Kernel/Migrate/d6/MigrateFieldWidgetSettingsTest.php index a16040f..e7bb636 100644 --- a/core/modules/field/tests/src/Kernel/Migrate/d6/MigrateFieldWidgetSettingsTest.php +++ b/core/modules/field/tests/src/Kernel/Migrate/d6/MigrateFieldWidgetSettingsTest.php @@ -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. diff --git a/core/modules/field_layout/config/schema/field_layout.schema.yml b/core/modules/field_layout/config/schema/field_layout.schema.yml new file mode 100644 index 0000000..3460b05 --- /dev/null +++ b/core/modules/field_layout/config/schema/field_layout.schema.yml @@ -0,0 +1,13 @@ +core.entity_view_display.*.*.*.third_party.field_layout: + type: field_layout.third_party_settings + +core.entity_form_display.*.*.*.third_party.field_layout: + type: field_layout.third_party_settings + +field_layout.third_party_settings: + type: mapping + label: 'Per-view mode field layout settings' + mapping: + layout: + type: string + label: 'Layout' diff --git a/core/modules/field_layout/css/twocol.layout.css b/core/modules/field_layout/css/twocol.layout.css new file mode 100644 index 0000000..6803d5b --- /dev/null +++ b/core/modules/field_layout/css/twocol.layout.css @@ -0,0 +1,8 @@ +.field-layout--twocol { + display: flex; + flex-wrap: wrap; + justify-content: space-between; +} +.field-layout--twocol > .field-layout-region { + flex: 0 1 50%; +} diff --git a/core/modules/field_layout/field_layout.info.yml b/core/modules/field_layout/field_layout.info.yml new file mode 100644 index 0000000..62f225a --- /dev/null +++ b/core/modules/field_layout/field_layout.info.yml @@ -0,0 +1,6 @@ +name: 'Field Layout' +type: module +description: 'Adds layout capabilities to the Field UI.' +package: Core (Experimental) +version: VERSION +core: 8.x diff --git a/core/modules/field_layout/field_layout.install b/core/modules/field_layout/field_layout.install new file mode 100644 index 0000000..456054e --- /dev/null +++ b/core/modules/field_layout/field_layout.install @@ -0,0 +1,26 @@ +save(); + }; + array_map($entity_save, EntityViewDisplay::loadMultiple()); + array_map($entity_save, EntityFormDisplay::loadMultiple()); + + // Invalidate the render cache since all content will now have a layout. + Cache::invalidateTags(['rendered']); +} diff --git a/core/modules/field_layout/field_layout.libraries.yml b/core/modules/field_layout/field_layout.libraries.yml new file mode 100644 index 0000000..9798509 --- /dev/null +++ b/core/modules/field_layout/field_layout.libraries.yml @@ -0,0 +1,5 @@ +drupal.field_layout.twocol: + version: VERSION + css: + layout: + css/twocol.layout.css: {} diff --git a/core/modules/field_layout/field_layout.module b/core/modules/field_layout/field_layout.module new file mode 100644 index 0000000..0f7b16f --- /dev/null +++ b/core/modules/field_layout/field_layout.module @@ -0,0 +1,181 @@ +' . t('About') . ''; + $output .= '

' . t('The Field Layout module allows you to arrange fields into regions for content forms and displays.') . '

'; + $output .= '

' . t('For more information, see the online documentation for the Field Layout module.', [':field-layout-documentation' => 'https://www.drupal.org/documentation/modules/@todo_once_module_name_is_decided_upon']) . '

'; + return $output; + } +} + +/** + * Implements hook_entity_type_alter(). + */ +function field_layout_entity_type_alter(array &$entity_types) { + /** @var $entity_types \Drupal\Core\Entity\EntityTypeInterface[] */ + $entity_types['entity_view_display']->setClass(FieldLayoutEntityViewDisplay::class); + $entity_types['entity_form_display']->setClass(FieldLayoutEntityFormDisplay::class); + + // The form classes are only needed when Field UI is installed. + if (\Drupal::moduleHandler()->moduleExists('field_ui')) { + $entity_types['entity_view_display']->setFormClass('edit', FieldLayoutEntityViewDisplayEditForm::class); + $entity_types['entity_form_display']->setFormClass('edit', FieldLayoutEntityFormDisplayEditForm::class); + } +} + +/** + * Implements hook_entity_presave(). + */ +function field_layout_entity_presave(EntityInterface $entity) { + // Whenever creating a new entity display, set the layout to default. + if ($entity instanceof EntityDisplayWithLayoutInterface && !$entity->getLayoutId()) { + $entity->setLayoutId('default'); + } +} + +/** + * Implements hook_entity_view_alter(). + */ +function field_layout_entity_view_alter(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display) { + if ($display instanceof EntityDisplayWithLayoutInterface) { + _field_layout_apply_layout($build, $display); + } +} + +/** + * Implements hook_form_alter(). + */ +function field_layout_form_alter(&$form, FormStateInterface $form_state, $form_id) { + $form_object = $form_state->getFormObject(); + if ($form_object instanceof ContentEntityFormInterface && $display = $form_object->getFormDisplay($form_state)) { + if ($display instanceof EntityDisplayWithLayoutInterface) { + _field_layout_apply_layout($form, $display, TRUE); + } + } +} + +/** + * Applies the layout to an entity build. + * + * @param array $build + * A renderable array representing the entity content or form. + * @param \Drupal\field_layout\Display\EntityDisplayWithLayoutInterface $display + * The entity display holding the display options configured for the + * entity components. + * @param bool $in_form_context + * (optional) If in a form context, an alternate method will be used to render + * fields in their regions. Defaults to FALSE. + */ +function _field_layout_apply_layout(array &$build, EntityDisplayWithLayoutInterface $display, $in_form_context = FALSE) { + if (!$layout_definition = \Drupal::service('field_layout.layout_repository')->getLayoutForDisplay($display)) { + return; + } + + // Wrap the regions in a layout element. + $build['field_layout']['#theme_wrappers'][] = 'field_layout_layout'; + $build['field_layout']['#layout'] = $layout_definition['layout']; + if (isset($layout_definition['library'])) { + $build['#attached']['library'][] = $layout_definition['library']; + } + + // Add the regions to the $build in the correct order. + foreach ($layout_definition['regions'] as $region => $region_info) { + $region_build = [ + '#theme_wrappers' => ['field_layout_region'], + '#region' => $region, + ]; + if ($in_form_context) { + $region_build['#process'][] = '\Drupal\Core\Render\Element\RenderElement::processGroup'; + $region_build['#pre_render'][] = '\Drupal\Core\Render\Element\RenderElement::preRenderGroup'; + } + $build['field_layout']['field_layout__' . $region] = $region_build; + } + + /** @var \Drupal\Core\Field\FieldDefinitionInterface[] $field_definitions */ + $field_definitions = \Drupal::service('entity_field.manager')->getFieldDefinitions($display->getTargetEntityTypeId(), $display->getTargetBundle()); + // Move the field from the top-level of $build into a region-specific section. + foreach ($display->getComponents() as $name => $field) { + // If the component is a true field, but not configurable, do not alter it. + if (isset($field_definitions[$name]) && !$field_definitions[$name]->isDisplayConfigurable($display->get('displayContext'))) { + continue; + } + + if (isset($build[$name]) && isset($field['region'])) { + // If this is a form, #group can be used to relocate the fields. This + // avoids breaking hook_form_alter() implementations by not actually + // moving the field in the form structure. + if ($in_form_context) { + $build[$name]['#group'] = 'field_layout__' . $field['region']; + } + else { + $build['field_layout']['field_layout__' . $field['region']][$name] = $build[$name]; + unset($build[$name]); + } + } + } +} + +/** + * Implements hook_theme(). + */ +function field_layout_theme($existing, $type, $theme, $path) { + return [ + 'field_layout_region' => [ + 'render element' => 'elements', + ], + 'field_layout_layout' => [ + 'render element' => 'elements', + ], + ]; +} + +/** + * Prepares variables for field layout layout templates. + * + * Default template: field-layout-layout.html.twig. + * + * @param array $variables + * An associative array containing: + * - elements: An associative array containing properties of the layout. + */ +function template_preprocess_field_layout_layout(&$variables) { + // Create the $content variable that templates expect. + $variables['content'] = $variables['elements']['#children']; + $variables['layout'] = $variables['elements']['#layout']; +} + +/** + * Prepares variables for field layout region templates. + * + * Default template: field-layout-region.html.twig. + * + * @param array $variables + * An associative array containing: + * - elements: An associative array containing properties of the region. + */ +function template_preprocess_field_layout_region(&$variables) { + // Create the $content variable that templates expect. + $variables['content'] = $variables['elements']['#children']; + $variables['region'] = $variables['elements']['#region']; +} diff --git a/core/modules/field_layout/field_layout.services.yml b/core/modules/field_layout/field_layout.services.yml new file mode 100644 index 0000000..b4082ff --- /dev/null +++ b/core/modules/field_layout/field_layout.services.yml @@ -0,0 +1,4 @@ +services: + field_layout.layout_repository: + class: Drupal\field_layout\LayoutRepository + arguments: ['@string_translation'] diff --git a/core/modules/field_layout/src/Display/EntityDisplayWithLayoutInterface.php b/core/modules/field_layout/src/Display/EntityDisplayWithLayoutInterface.php new file mode 100644 index 0000000..f98087f --- /dev/null +++ b/core/modules/field_layout/src/Display/EntityDisplayWithLayoutInterface.php @@ -0,0 +1,38 @@ +getThirdPartySetting('field_layout', 'layout'); + } + + /** + * Implements \Drupal\field_layout\Display\EntityDisplayWithLayoutInterface::setLayoutId(). + */ + public function setLayoutId($layout_id) { + $this->setThirdPartySetting('field_layout', 'layout', $layout_id); + return $this; + } + +} diff --git a/core/modules/field_layout/src/Entity/FieldLayoutEntityFormDisplay.php b/core/modules/field_layout/src/Entity/FieldLayoutEntityFormDisplay.php new file mode 100644 index 0000000..641427f --- /dev/null +++ b/core/modules/field_layout/src/Entity/FieldLayoutEntityFormDisplay.php @@ -0,0 +1,26 @@ +getLayoutForDisplay($this); + return key($layout_definition['regions']); + } + +} diff --git a/core/modules/field_layout/src/Entity/FieldLayoutEntityViewDisplay.php b/core/modules/field_layout/src/Entity/FieldLayoutEntityViewDisplay.php new file mode 100644 index 0000000..025b39b --- /dev/null +++ b/core/modules/field_layout/src/Entity/FieldLayoutEntityViewDisplay.php @@ -0,0 +1,26 @@ +getLayoutForDisplay($this); + return key($layout_definition['regions']); + } + +} diff --git a/core/modules/field_layout/src/Form/FieldLayoutEntityDisplayFormTrait.php b/core/modules/field_layout/src/Form/FieldLayoutEntityDisplayFormTrait.php new file mode 100644 index 0000000..83e7c88 --- /dev/null +++ b/core/modules/field_layout/src/Form/FieldLayoutEntityDisplayFormTrait.php @@ -0,0 +1,116 @@ +fieldLayoutRepository->getLayoutForDisplay($this->getEntity()); + foreach ($layout['regions'] as $name => $region) { + $regions[$name] = [ + 'title' => $region['label'], + 'message' => $this->t('No field is displayed.') + ]; + } + + $regions['hidden'] = [ + 'title' => $this->t('Disabled', [], ['context' => 'Plural']), + 'message' => $this->t('No field is hidden.') + ]; + + return $regions; + } + + /** + * Adds the field layout section to the form. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @return array + */ + protected function addFieldLayout(array $form, FormStateInterface $form_state) { + $form['#entity_builders'][] = [static::class, 'updateFieldLayout']; + + $form['field_layouts'] = [ + '#type' => 'details', + '#title' => $this->t('Layout for @bundle in @view_mode', [ + '@bundle' => str_replace('_', ' ', $this->getEntity()->getTargetBundle()), + '@view_mode' => str_replace('_', ' ', $this->getEntity()->getMode()), + ]), + ]; + $layout_options = []; + foreach ($this->fieldLayoutRepository->getLayoutDefinitions() as $name => $layout) { + $layout_options[$name] = $layout['label']; + } + $form['field_layouts']['field_layout'] = [ + '#type' => 'select', + '#title' => $this->t('Select a layout'), + '#options' => $layout_options, + '#default_value' => $this->getEntity()->getLayoutId() ?: 'default', + ]; + + return $form; + } + + /** + * An #entity_builder callback to update the Field Layout settings. + * + * @param string $entity_type_id + * @param \Drupal\Core\Entity\EntityInterface $entity + * @param array $form + * @param \Drupal\Core\Form\FormStateInterface $form_state + */ + public static function updateFieldLayout($entity_type_id, EntityInterface $entity, &$form, FormStateInterface $form_state) { + if ($form_state->isSubmitted() && $entity instanceof EntityDisplayWithLayoutInterface) { + $old_layout = $entity->getLayoutId(); + $new_layout = $form_state->getValue('field_layout'); + $entity->setLayoutId($new_layout); + // If the layout is changing, reset all fields. + if ($new_layout !== $old_layout) { + // @todo Devise a mechanism for mapping old regions to new ones in + // https://www.drupal.org/node/2796877. + $new_region = $entity->getDefaultRegion(); + foreach ($form_state->getValue('fields') as $field_name => $values) { + if (($component = $entity->getComponent($field_name)) && $new_region !== 'hidden') { + $component['region'] = $new_region; + $entity->setComponent($field_name, $component); + } + else { + $entity->removeComponent($field_name); + } + } + } + } + } + + /** + * Gets the form entity. + * + * @return \Drupal\field_layout\Display\EntityDisplayWithLayoutInterface + * The current form entity. + */ + abstract public function getEntity(); + +} diff --git a/core/modules/field_layout/src/Form/FieldLayoutEntityFormDisplayEditForm.php b/core/modules/field_layout/src/Form/FieldLayoutEntityFormDisplayEditForm.php new file mode 100644 index 0000000..d57912e --- /dev/null +++ b/core/modules/field_layout/src/Form/FieldLayoutEntityFormDisplayEditForm.php @@ -0,0 +1,54 @@ +fieldLayoutRepository = $field_layout_repository; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('plugin.manager.field.field_type'), + $container->get('plugin.manager.field.widget'), + $container->get('field_layout.layout_repository') + ); + } + + /** + * {@inheritdoc} + */ + public function form(array $form, FormStateInterface $form_state) { + $form = parent::form($form, $form_state); + return $this->addFieldLayout($form, $form_state); + } + +} diff --git a/core/modules/field_layout/src/Form/FieldLayoutEntityViewDisplayEditForm.php b/core/modules/field_layout/src/Form/FieldLayoutEntityViewDisplayEditForm.php new file mode 100644 index 0000000..5c9f26f --- /dev/null +++ b/core/modules/field_layout/src/Form/FieldLayoutEntityViewDisplayEditForm.php @@ -0,0 +1,54 @@ +fieldLayoutRepository = $field_layout_repository; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('plugin.manager.field.field_type'), + $container->get('plugin.manager.field.formatter'), + $container->get('field_layout.layout_repository') + ); + } + + /** + * {@inheritdoc} + */ + public function form(array $form, FormStateInterface $form_state) { + $form = parent::form($form, $form_state); + return $this->addFieldLayout($form, $form_state); + } + +} diff --git a/core/modules/field_layout/src/LayoutRepository.php b/core/modules/field_layout/src/LayoutRepository.php new file mode 100644 index 0000000..d0e5630 --- /dev/null +++ b/core/modules/field_layout/src/LayoutRepository.php @@ -0,0 +1,72 @@ +stringTranslation = $string_translation; + } + + /** + * {@inheritdoc} + */ + public function getLayoutDefinitions() { + // @todo Replace with layout_plugin in https://www.drupal.org/node/2296423. + $layouts = [ + 'default' => [ + 'label' => $this->t('Default'), + 'regions' => [ + 'content' => [ + 'label' => $this->t('Content'), + ], + ], + ], + 'twocol' => [ + 'label' => $this->t('Two column'), + 'library' => 'field_layout/drupal.field_layout.twocol', + 'regions' => [ + 'left' => [ + 'label' => $this->t('Left'), + ], + 'right' => [ + 'label' => $this->t('Right'), + ], + ], + ], + ]; + foreach ($layouts as $layout_id => $layout) { + $layouts[$layout_id]['layout'] = $layout_id; + } + return $layouts; + } + + /** + * {@inheritdoc} + */ + public function getLayoutForDisplay(EntityDisplayWithLayoutInterface $display) { + $layout_id = $display->getLayoutId() ?: 'default'; + $layout_definitions = $this->getLayoutDefinitions(); + if (!$layout_id || !isset($layout_definitions[$layout_id])) { + return []; + } + + return $layout_definitions[$layout_id]; + } + +} diff --git a/core/modules/field_layout/src/LayoutRepositoryInterface.php b/core/modules/field_layout/src/LayoutRepositoryInterface.php new file mode 100644 index 0000000..2555852 --- /dev/null +++ b/core/modules/field_layout/src/LayoutRepositoryInterface.php @@ -0,0 +1,31 @@ +. + * - layout: The name of the layout variable. + * + * @ingroup themeable + */ +#} +{% +set classes = [ +'field-layout--' ~ layout|clean_class, +] +%} +{% if content %} + + {{ content }} + +{% endif %} diff --git a/core/modules/field_layout/templates/field-layout-region.html.twig b/core/modules/field_layout/templates/field-layout-region.html.twig new file mode 100644 index 0000000..11e3604 --- /dev/null +++ b/core/modules/field_layout/templates/field-layout-region.html.twig @@ -0,0 +1,24 @@ +{# +/** + * @file + * Default theme implementation to display a region. + * + * Available variables: + * - content: The content for this region, typically blocks. + * - attributes: HTML attributes for the region
. + * - region: The name of the region variable. + * + * @ingroup themeable + */ +#} +{% +set classes = [ +'field-layout-region', +'field-layout-region--' ~ region|clean_class, +] +%} +{% if content %} + + {{ content }} +
+{% endif %} diff --git a/core/modules/field_layout/tests/modules/field_layout_test/field_layout_test.info.yml b/core/modules/field_layout/tests/modules/field_layout_test/field_layout_test.info.yml new file mode 100644 index 0000000..4d699e4 --- /dev/null +++ b/core/modules/field_layout/tests/modules/field_layout_test/field_layout_test.info.yml @@ -0,0 +1,8 @@ +name: 'Field Layout test' +type: module +description: 'Support module for Field Layout tests.' +core: 8.x +package: Testing +version: VERSION +dependencies: + - entity_test diff --git a/core/modules/field_layout/tests/modules/field_layout_test/field_layout_test.routing.yml b/core/modules/field_layout/tests/modules/field_layout_test/field_layout_test.routing.yml new file mode 100644 index 0000000..bcea288 --- /dev/null +++ b/core/modules/field_layout/tests/modules/field_layout_test/field_layout_test.routing.yml @@ -0,0 +1,7 @@ +entity.entity_test.test_view_mode: + path: '/entity_test/{entity_test}/test' + defaults: + _entity_view: 'entity_test.test' + _title: 'Test test view mode' + requirements: + _entity_access: 'entity_test.view' diff --git a/core/modules/field_layout/tests/src/FunctionalJavascript/FieldLayoutTest.php b/core/modules/field_layout/tests/src/FunctionalJavascript/FieldLayoutTest.php new file mode 100644 index 0000000..04d0810 --- /dev/null +++ b/core/modules/field_layout/tests/src/FunctionalJavascript/FieldLayoutTest.php @@ -0,0 +1,253 @@ +createContentType([ + 'type' => 'article', + ]); + $this->createNode([ + 'type' => 'article', + 'title' => 'The node title', + 'body' => [[ + 'value' => 'The node body', + ]], + ]); + $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', + 'administer content types', + 'administer nodes', + 'administer node fields', + 'administer node display', + 'administer node form display', + '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 that layouts are unique per-view mode. + */ + public function testEntityViewModes() { + // By default, the field is not visible. + $this->drupalGet('entity_test/1/test'); + $this->assertSession()->elementNotExists('css', '.field-layout-region--content .field--name-field-test-text'); + $this->drupalGet('entity_test/1'); + $this->assertSession()->elementNotExists('css', '.field-layout-region--content .field--name-field-test-text'); + + // Change the layout for the "test" view mode. See + // core.entity_view_mode.entity_test.test.yml. + $this->drupalGet('entity_test/structure/entity_test/display'); + $this->click('#edit-modes'); + $this->getSession()->getPage()->checkField('display_modes_custom[test]'); + $this->submitForm([], 'Save'); + $this->clickLink('configure them'); + $this->getSession()->getPage()->pressButton('Show row weights'); + $this->getSession()->getPage()->selectFieldOption('fields[field_test_text][region]', 'content'); + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->submitForm([], 'Save'); + + // Each view mode has a different layout. + $this->drupalGet('entity_test/1/test'); + $this->assertSession()->elementExists('css', '.field-layout-region--content .field--name-field-test-text'); + $this->drupalGet('entity_test/1'); + $this->assertSession()->elementNotExists('css', '.field-layout-region--content .field--name-field-test-text'); + } + + /** + * Tests the use of field layout for entity form displays. + */ + public function testEntityForm() { + // By default, the default layout is used. + $this->drupalGet('entity_test/manage/1/edit'); + $this->assertFieldInRegion('field_test_text[0][value]', 'content'); + + // The default layout is in use. + $this->drupalGet('entity_test/structure/entity_test/form-display'); + $this->assertEquals(['Content', 'Disabled'], $this->getRegionTitles()); + + // Switch the layout to two columns. + $this->click('#edit-field-layouts'); + $this->getSession()->getPage()->selectFieldOption('field_layout', 'twocol'); + $this->submitForm([], 'Save'); + + // The field is moved to the default region for the new layout. + $this->assertSession()->pageTextContains('Your settings have been saved.'); + $this->assertEquals(['Left', 'Right', 'Disabled'], $this->getRegionTitles()); + + $this->drupalGet('entity_test/manage/1/edit'); + // No fields are visible, and the regions don't display when empty. + $this->assertFieldInRegion('field_test_text[0][value]', 'left'); + $this->assertSession()->elementExists('css', '.field-layout-region--left .field--name-field-test-text'); + + // After a refresh the new regions are still there. + $this->drupalGet('entity_test/structure/entity_test/form-display'); + $this->assertEquals(['Left', 'Right', 'Disabled'], $this->getRegionTitles()); + + // Drag the field to the right region. + $field_test_text_row = $this->getSession()->getPage()->find('css', '#field-test-text'); + $left_region_row = $this->getSession()->getPage()->find('css', '.region-right-message'); + $field_test_text_row->find('css', '.handle')->dragTo($left_region_row); + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->submitForm([], 'Save'); + $this->assertSession()->pageTextContains('Your settings have been saved.'); + + // The new layout is used. + $this->drupalGet('entity_test/manage/1/edit'); + $this->assertSession()->elementExists('css', '.field-layout-region--right .field--name-field-test-text'); + $this->assertFieldInRegion('field_test_text[0][value]', 'right'); + + // Move the field to the right region without tabledrag. + $this->drupalGet('entity_test/structure/entity_test/form-display'); + $this->getSession()->getPage()->pressButton('Show row weights'); + $this->getSession()->getPage()->selectFieldOption('fields[field_test_text][region]', 'right'); + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->submitForm([], 'Save'); + $this->assertSession()->pageTextContains('Your settings have been saved.'); + + // The updated region is used. + $this->drupalGet('entity_test/manage/1/edit'); + $this->assertFieldInRegion('field_test_text[0][value]', 'right'); + + // The layout is still in use without Field UI. + $this->container->get('module_installer')->uninstall(['field_ui']); + $this->drupalGet('entity_test/manage/1/edit'); + $this->assertFieldInRegion('field_test_text[0][value]', 'right'); + } + + /** + * Tests the use of field layout for entity view displays. + */ + public function testEntityView() { + // The default layout is in use. + $this->drupalGet('entity_test/structure/entity_test/display'); + $this->assertEquals(['Content', 'Disabled'], $this->getRegionTitles()); + + // Switch the layout to two columns. + $this->click('#edit-field-layouts'); + $this->getSession()->getPage()->selectFieldOption('field_layout', 'twocol'); + $this->submitForm([], 'Save'); + + $this->assertSession()->pageTextContains('Your settings have been saved.'); + $this->assertEquals(['Left', 'Right', 'Disabled'], $this->getRegionTitles()); + + $this->drupalGet('entity_test/1'); + // No fields are visible, and the regions don't display when empty. + $this->assertSession()->elementNotExists('css', '.field-layout--twocol'); + $this->assertSession()->elementNotExists('css', '.field-layout-region'); + $this->assertSession()->elementNotExists('css', '.field--name-field-test-text'); + + // After a refresh the new regions are still there. + $this->drupalGet('entity_test/structure/entity_test/display'); + $this->assertEquals(['Left', 'Right', 'Disabled'], $this->getRegionTitles()); + + // Drag the field to the left region. + $this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][type]', 'hidden')->isSelected()); + $field_test_text_row = $this->getSession()->getPage()->find('css', '#field-test-text'); + $left_region_row = $this->getSession()->getPage()->find('css', '.region-left-message'); + $field_test_text_row->find('css', '.handle')->dragTo($left_region_row); + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->assertFalse($this->assertSession()->optionExists('fields[field_test_text][type]', 'hidden')->isSelected()); + $this->submitForm([], 'Save'); + $this->assertSession()->pageTextContains('Your settings have been saved.'); + + // The new layout is used. + $this->drupalGet('entity_test/1'); + $this->assertSession()->elementExists('css', '.field-layout--twocol'); + $this->assertSession()->elementExists('css', '.field-layout-region--left .field--name-field-test-text'); + + // Move the field to the right region without tabledrag. + $this->drupalGet('entity_test/structure/entity_test/display'); + $this->getSession()->getPage()->pressButton('Show row weights'); + $this->getSession()->getPage()->selectFieldOption('fields[field_test_text][region]', 'right'); + $this->assertSession()->assertWaitOnAjaxRequest(); + $this->submitForm([], 'Save'); + $this->assertSession()->pageTextContains('Your settings have been saved.'); + + // The updated region is used. + $this->drupalGet('entity_test/1'); + $this->assertSession()->elementExists('css', '.field-layout-region--right .field--name-field-test-text'); + + // The layout is still in use without Field UI. + $this->container->get('module_installer')->uninstall(['field_ui']); + $this->drupalGet('entity_test/1'); + $this->assertSession()->elementExists('css', '.field-layout--twocol'); + $this->assertSession()->elementExists('css', '.field-layout-region--right .field--name-field-test-text'); + } + + /** + * Tests an entity type that has fields shown by default. + */ + public function testNodeView() { + // By default, the default layout is used. + $this->drupalGet('node/1'); + $this->assertSession()->elementExists('css', '.field-layout--default'); + $this->assertSession()->elementExists('css', '.field-layout-region--content .field--name-body'); + + $this->drupalGet('admin/structure/types/manage/article/display'); + $this->assertEquals(['Content', 'Disabled'], $this->getRegionTitles()); + $this->assertSession()->optionExists('fields[body][region]', 'content'); + } + + /** + * Gets the region titles on the page. + * + * @return string[] + * An array of region titles. + */ + protected function getRegionTitles() { + $region_titles = []; + $region_title_elements = $this->getSession()->getPage()->findAll('css', '.region-title td'); + /** @var \Behat\Mink\Element\NodeElement[] $region_title_elements */ + foreach ($region_title_elements as $region_title_element) { + $region_titles[] = $region_title_element->getText(); + } + return $region_titles; + } + + /** + * Asserts that a field exists in a given region. + * + * @param string $field_selector + * The field selector, one of field id|name|label|value. + * @param string $region_name + * The machine name of the region. + */ + protected function assertFieldInRegion($field_selector, $region_name) { + $region_element = $this->getSession()->getPage()->find('css', ".field-layout-region--$region_name"); + $this->assertNotNull($region_element); + $this->assertSession()->fieldExists($field_selector, $region_element); + } + +} diff --git a/core/modules/field_layout/tests/src/Unit/LayoutRepositoryTest.php b/core/modules/field_layout/tests/src/Unit/LayoutRepositoryTest.php new file mode 100644 index 0000000..bf55e11 --- /dev/null +++ b/core/modules/field_layout/tests/src/Unit/LayoutRepositoryTest.php @@ -0,0 +1,59 @@ +getStringTranslationStub()); + + $expected = ['default', 'twocol']; + + $layout_definitions = $layout_repository->getLayoutDefinitions(); + $this->assertEquals($expected, array_keys($layout_definitions)); + } + + /** + * @covers ::getLayoutForDisplay + */ + public function testGetLayoutForDisplay() { + $layout_repository = new LayoutRepository($this->getStringTranslationStub()); + + $display = $this->prophesize(EntityDisplayWithLayoutInterface::class); + $display->getLayoutId()->willReturn('twocol'); + + $expected = ['left', 'right']; + + $layout_definition = $layout_repository->getLayoutForDisplay($display->reveal()); + $this->assertSame('twocol', $layout_definition['layout']); + $this->assertEquals($expected, array_keys($layout_definition['regions'])); + } + + /** + * @covers ::getLayoutForDisplay + */ + public function testGetLayoutForDisplayEmpty() { + $layout_repository = new LayoutRepository($this->getStringTranslationStub()); + + $display = $this->prophesize(EntityDisplayWithLayoutInterface::class); + $display->getLayoutId()->willReturn(NULL); + + $expected = ['content']; + + $layout_definition = $layout_repository->getLayoutForDisplay($display->reveal()); + $this->assertSame('default', $layout_definition['layout']); + $this->assertEquals($expected, array_keys($layout_definition['regions'])); + } + +} diff --git a/core/modules/field_ui/field_ui.js b/core/modules/field_ui/field_ui.js index a53553d..e11a2e8 100644 --- a/core/modules/field_ui/field_ui.js +++ b/core/modules/field_ui/field_ui.js @@ -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 @@ else if ($this.is('.region-empty')) { 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 @@ else if ($this.is('.region-empty')) { * Either 'hidden' or 'content'. */ getRegion: function () { + return this.$regionSelect.val(); + }, + + /** + * Returns the region corresponding to the current form values of the row. + * + * @returns {string} + * Either 'hidden' or 'content'. + */ + getType: function () { return (this.$pluginSelect.val() === 'hidden') ? 'hidden' : 'content'; }, @@ -305,14 +324,17 @@ else if ($this.is('.region-empty')) { * {@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 diff --git a/core/modules/field_ui/src/Form/EntityDisplayFormBase.php b/core/modules/field_ui/src/Form/EntityDisplayFormBase.php index 586f1ef..f4fb39c 100644 --- a/core/modules/field_ui/src/Form/EntityDisplayFormBase.php +++ b/core/modules/field_ui/src/Form/EntityDisplayFormBase.php @@ -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'; } } diff --git a/core/modules/field_ui/src/Form/EntityFormDisplayEditForm.php b/core/modules/field_ui/src/Form/EntityFormDisplayEditForm.php index 741b98d..af8e2ed 100644 --- a/core/modules/field_ui/src/Form/EntityFormDisplayEditForm.php +++ b/core/modules/field_ui/src/Form/EntityFormDisplayEditForm.php @@ -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), ); } diff --git a/core/modules/field_ui/src/Form/EntityViewDisplayEditForm.php b/core/modules/field_ui/src/Form/EntityViewDisplayEditForm.php index f273325..174726f 100644 --- a/core/modules/field_ui/src/Form/EntityViewDisplayEditForm.php +++ b/core/modules/field_ui/src/Form/EntityViewDisplayEditForm.php @@ -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), ); diff --git a/core/modules/field_ui/tests/src/FunctionalJavascript/EntityDisplayTest.php b/core/modules/field_ui/tests/src/FunctionalJavascript/EntityDisplayTest.php new file mode 100644 index 0000000..7cde00c --- /dev/null +++ b/core/modules/field_ui/tests/src/FunctionalJavascript/EntityDisplayTest.php @@ -0,0 +1,95 @@ + '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'); + } + +} diff --git a/core/modules/field_ui/tests/src/Kernel/EntityDisplayTest.php b/core/modules/field_ui/tests/src/Kernel/EntityDisplayTest.php index be188db..756b9a7 100644 --- a/core/modules/field_ui/tests/src/Kernel/EntityDisplayTest.php +++ b/core/modules/field_ui/tests/src/Kernel/EntityDisplayTest.php @@ -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'); $display->setComponent('component_1'); $display->setComponent('component_2'); $this->assertEqual($display->getComponent('component_1'), $expected['component_1']); $this->assertEqual($display->getComponent('component_2'), $expected['component_2']); // Check that arbitrary options are correctly stored. - $expected['component_3'] = array('weight' => 10, 'third_party_settings' => array('field_test' => array('foo' => 'bar')), 'settings' => array()); + $expected['component_3'] = array('weight' => 10, 'third_party_settings' => array('field_test' => array('foo' => 'bar')), 'settings' => array(), 'region' => 'content'); $display->setComponent('component_3', $expected['component_3']); $this->assertEqual($display->getComponent('component_3'), $expected['component_3']); @@ -86,6 +86,7 @@ public function testEntityDisplayCRUD() { 'link_to_entity' => FALSE, ), 'third_party_settings' => array(), + 'region' => 'content', ); $this->assertEqual($display->getComponents(), $expected); @@ -148,7 +149,7 @@ public function testEntityGetDisplay() { $display = entity_get_display('entity_test', 'entity_test', 'default'); $this->assertFalse($display->isNew()); $this->assertEqual($display->id(), 'entity_test.entity_test.default'); - $this->assertEqual($display->getComponent('component_1'), array( 'weight' => 10, 'settings' => array(), 'third_party_settings' => array())); + $this->assertEqual($display->getComponent('component_1'), array( 'weight' => 10, 'settings' => array(), 'third_party_settings' => array(), 'region' => 'content')); } /** @@ -164,14 +165,14 @@ public function testExtraFieldComponent() { // Check that the default visibility taken into account for extra fields // unknown in the display. - $this->assertEqual($display->getComponent('display_extra_field'), array('weight' => 5)); + $this->assertEqual($display->getComponent('display_extra_field'), array('weight' => 5, 'type' => 'visible', 'region' => 'content')); $this->assertNull($display->getComponent('display_extra_field_hidden')); // Check that setting explicit options overrides the defaults. $display->removeComponent('display_extra_field'); $display->setComponent('display_extra_field_hidden', array('weight' => 10)); $this->assertNull($display->getComponent('display_extra_field')); - $this->assertEqual($display->getComponent('display_extra_field_hidden'), array('weight' => 10, 'settings' => array(), 'third_party_settings' => array())); + $this->assertEqual($display->getComponent('display_extra_field_hidden'), array('weight' => 10, 'settings' => array(), 'third_party_settings' => array(), 'region' => 'content')); } /** @@ -209,6 +210,7 @@ public function testFieldComponent() { 'type' => $default_formatter, 'settings' => $formatter_settings, 'third_party_settings' => array(), + 'region' => 'content', ); $this->assertEqual($display->getComponent($field_name), $expected); @@ -258,6 +260,7 @@ public function testBaseFieldComponent() { 'settings' => $formatter_settings, 'third_party_settings' => array(), 'weight' => 10, + 'region' => 'content', ), 'test_display_non_configurable' => array( 'label' => 'above', @@ -265,6 +268,7 @@ public function testBaseFieldComponent() { 'settings' => $formatter_settings, 'third_party_settings' => array(), 'weight' => 11, + 'region' => 'content', ), ); foreach ($expected as $field_name => $options) { diff --git a/core/modules/field_ui/tests/src/Kernel/EntityFormDisplayTest.php b/core/modules/field_ui/tests/src/Kernel/EntityFormDisplayTest.php index e38db7c..fb343fd 100644 --- a/core/modules/field_ui/tests/src/Kernel/EntityFormDisplayTest.php +++ b/core/modules/field_ui/tests/src/Kernel/EntityFormDisplayTest.php @@ -43,7 +43,7 @@ public function testEntityGetFromDisplay() { $form_display = entity_get_form_display('entity_test', 'entity_test', 'default'); $this->assertFalse($form_display->isNew()); $this->assertEqual($form_display->id(), 'entity_test.entity_test.default'); - $this->assertEqual($form_display->getComponent('component_1'), array('weight' => 10, 'settings' => array(), 'third_party_settings' => array())); + $this->assertEqual($form_display->getComponent('component_1'), array('weight' => 10, 'settings' => array(), 'third_party_settings' => array(), 'region' => 'content')); } /** @@ -80,6 +80,7 @@ public function testFieldComponent() { 'type' => $default_widget, 'settings' => $widget_settings, 'third_party_settings' => array(), + 'region' => 'content', ); $this->assertEqual($form_display->getComponent($field_name), $expected); @@ -134,12 +135,14 @@ public function testBaseFieldComponent() { 'settings' => $formatter_settings, 'third_party_settings' => array(), 'weight' => 10, + 'region' => 'content', ), 'test_display_non_configurable' => array( 'type' => 'text_textfield', 'settings' => $formatter_settings, 'third_party_settings' => array(), 'weight' => 11, + 'region' => 'content', ), ); foreach ($expected as $field_name => $options) { diff --git a/core/modules/forum/config/optional/core.entity_form_display.comment.comment_forum.default.yml b/core/modules/forum/config/optional/core.entity_form_display.comment.comment_forum.default.yml index a09c30b..4738bbb 100644 --- a/core/modules/forum/config/optional/core.entity_form_display.comment.comment_forum.default.yml +++ b/core/modules/forum/config/optional/core.entity_form_display.comment.comment_forum.default.yml @@ -13,9 +13,11 @@ mode: default content: author: weight: -2 + region: content comment_body: type: text_textarea weight: 11 + region: content settings: rows: 5 placeholder: '' @@ -23,6 +25,7 @@ content: subject: type: string_textfield weight: 10 + region: content settings: size: 60 placeholder: '' diff --git a/core/modules/forum/config/optional/core.entity_form_display.node.forum.default.yml b/core/modules/forum/config/optional/core.entity_form_display.node.forum.default.yml index c66ba23..6773d32 100644 --- a/core/modules/forum/config/optional/core.entity_form_display.node.forum.default.yml +++ b/core/modules/forum/config/optional/core.entity_form_display.node.forum.default.yml @@ -17,6 +17,7 @@ content: body: type: text_textarea_with_summary weight: 27 + region: content settings: rows: 9 summary_rows: 3 @@ -25,11 +26,13 @@ content: comment_forum: type: comment_default weight: 20 + region: content settings: { } third_party_settings: { } created: type: datetime_timestamp weight: 10 + region: content settings: { } third_party_settings: { } promote: @@ -37,21 +40,25 @@ 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: { } taxonomy_forums: type: options_select weight: 26 + region: content settings: { } third_party_settings: { } title: type: string_textfield weight: -5 + region: content settings: size: 60 placeholder: '' @@ -59,6 +66,7 @@ content: uid: type: entity_reference_autocomplete weight: 5 + region: content settings: match_operator: CONTAINS size: 60 diff --git a/core/modules/forum/config/optional/core.entity_form_display.taxonomy_term.forums.default.yml b/core/modules/forum/config/optional/core.entity_form_display.taxonomy_term.forums.default.yml index b18c869..50df98a 100644 --- a/core/modules/forum/config/optional/core.entity_form_display.taxonomy_term.forums.default.yml +++ b/core/modules/forum/config/optional/core.entity_form_display.taxonomy_term.forums.default.yml @@ -14,11 +14,13 @@ content: description: type: text_textfield weight: 0 + region: content settings: { } third_party_settings: { } name: type: string_textfield weight: -5 + region: content settings: size: 60 placeholder: '' diff --git a/core/modules/forum/config/optional/core.entity_view_display.comment.comment_forum.default.yml b/core/modules/forum/config/optional/core.entity_view_display.comment.comment_forum.default.yml index f4f0112..befeba8 100644 --- a/core/modules/forum/config/optional/core.entity_view_display.comment.comment_forum.default.yml +++ b/core/modules/forum/config/optional/core.entity_view_display.comment.comment_forum.default.yml @@ -15,8 +15,10 @@ content: label: hidden type: text_default weight: 0 + region: content settings: { } third_party_settings: { } links: weight: 100 + region: content hidden: { } diff --git a/core/modules/forum/config/optional/core.entity_view_display.node.forum.default.yml b/core/modules/forum/config/optional/core.entity_view_display.node.forum.default.yml index b157c83..f3e8c5c 100644 --- a/core/modules/forum/config/optional/core.entity_view_display.node.forum.default.yml +++ b/core/modules/forum/config/optional/core.entity_view_display.node.forum.default.yml @@ -20,21 +20,25 @@ content: label: hidden type: text_default weight: 0 + region: content settings: { } third_party_settings: { } comment_forum: label: hidden type: comment_default weight: 20 + region: content settings: view_mode: default pager_id: 0 third_party_settings: { } links: weight: 100 + region: content taxonomy_forums: type: entity_reference_label weight: -1 + region: content label: above settings: link: true diff --git a/core/modules/forum/config/optional/core.entity_view_display.node.forum.teaser.yml b/core/modules/forum/config/optional/core.entity_view_display.node.forum.teaser.yml index 4405e71..7b174f4 100644 --- a/core/modules/forum/config/optional/core.entity_view_display.node.forum.teaser.yml +++ b/core/modules/forum/config/optional/core.entity_view_display.node.forum.teaser.yml @@ -19,14 +19,17 @@ content: label: hidden type: text_summary_or_trimmed weight: 100 + region: content settings: trim_length: 600 third_party_settings: { } links: weight: 101 + region: content taxonomy_forums: type: entity_reference_label weight: 10 + region: content label: above settings: link: true diff --git a/core/modules/forum/config/optional/core.entity_view_display.taxonomy_term.forums.default.yml b/core/modules/forum/config/optional/core.entity_view_display.taxonomy_term.forums.default.yml index d1242d9..b326039 100644 --- a/core/modules/forum/config/optional/core.entity_view_display.taxonomy_term.forums.default.yml +++ b/core/modules/forum/config/optional/core.entity_view_display.taxonomy_term.forums.default.yml @@ -14,6 +14,7 @@ content: description: type: text_default weight: 0 + region: content settings: { } third_party_settings: { } label: above diff --git a/core/modules/options/tests/options_config_install_test/config/install/core.entity_form_display.node.options_install_test.default.yml b/core/modules/options/tests/options_config_install_test/config/install/core.entity_form_display.node.options_install_test.default.yml index 19a2ef7..ff5f0ec 100644 --- a/core/modules/options/tests/options_config_install_test/config/install/core.entity_form_display.node.options_install_test.default.yml +++ b/core/modules/options/tests/options_config_install_test/config/install/core.entity_form_display.node.options_install_test.default.yml @@ -14,6 +14,7 @@ content: title: type: string_textfield weight: -5 + region: content settings: size: 60 placeholder: '' @@ -21,6 +22,7 @@ content: uid: type: entity_reference_autocomplete weight: 5 + region: content settings: match_operator: CONTAINS size: 60 @@ -29,6 +31,7 @@ content: created: type: datetime_timestamp weight: 10 + region: content settings: { } third_party_settings: { } promote: @@ -36,16 +39,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: { } body: type: text_textarea_with_summary weight: 26 + region: content settings: rows: 9 summary_rows: 3 diff --git a/core/modules/options/tests/options_config_install_test/config/install/core.entity_view_display.node.options_install_test.default.yml b/core/modules/options/tests/options_config_install_test/config/install/core.entity_view_display.node.options_install_test.default.yml index c107b10..aaea1cb 100644 --- a/core/modules/options/tests/options_config_install_test/config/install/core.entity_view_display.node.options_install_test.default.yml +++ b/core/modules/options/tests/options_config_install_test/config/install/core.entity_view_display.node.options_install_test.default.yml @@ -14,10 +14,12 @@ mode: default content: links: weight: 100 + region: content body: label: hidden type: text_default weight: 101 + region: content settings: { } third_party_settings: { } hidden: diff --git a/core/modules/options/tests/options_config_install_test/config/install/core.entity_view_display.node.options_install_test.teaser.yml b/core/modules/options/tests/options_config_install_test/config/install/core.entity_view_display.node.options_install_test.teaser.yml index 3b472a7..6e79af9 100644 --- a/core/modules/options/tests/options_config_install_test/config/install/core.entity_view_display.node.options_install_test.teaser.yml +++ b/core/modules/options/tests/options_config_install_test/config/install/core.entity_view_display.node.options_install_test.teaser.yml @@ -15,10 +15,12 @@ mode: teaser content: links: weight: 100 + region: content body: label: hidden type: text_summary_or_trimmed weight: 101 + region: content settings: trim_length: 600 third_party_settings: { } diff --git a/core/modules/system/src/Tests/Update/UpdateEntityDisplayTest.php b/core/modules/system/src/Tests/Update/UpdateEntityDisplayTest.php new file mode 100644 index 0000000..0a2cbf4 --- /dev/null +++ b/core/modules/system/src/Tests/Update/UpdateEntityDisplayTest.php @@ -0,0 +1,49 @@ +databaseDumpFiles = [ + __DIR__ . '/../../../tests/fixtures/update/drupal-8.bare.standard.php.gz', + ]; + } + + /** + * Tests that entity displays are updated with regions for their fields. + */ + public function testUpdate() { + // No region key appears pre-update. + $entity_form_display = EntityFormDisplay::load('node.article.default'); + $options = $entity_form_display->getComponent('body'); + $this->assertFalse(array_key_exists('region', $options)); + + $entity_view_display = EntityViewDisplay::load('node.article.default'); + $options = $entity_view_display->getComponent('body'); + $this->assertFalse(array_key_exists('region', $options)); + + $this->runUpdates(); + + // The region key has been populated with 'content'. + $entity_form_display = EntityFormDisplay::load('node.article.default'); + $options = $entity_form_display->getComponent('body'); + $this->assertIdentical('content', $options['region']); + + $entity_view_display = EntityViewDisplay::load('node.article.default'); + $options = $entity_view_display->getComponent('body'); + $this->assertIdentical('content', $options['region']); + } + +} diff --git a/core/modules/system/system.post_update.php b/core/modules/system/system.post_update.php index b75625c..1bd11a8 100644 --- a/core/modules/system/system.post_update.php +++ b/core/modules/system/system.post_update.php @@ -5,6 +5,10 @@ * Post update functions for System. */ +use Drupal\Core\Entity\Display\EntityDisplayInterface; +use Drupal\Core\Entity\Entity\EntityFormDisplay; +use Drupal\Core\Entity\Entity\EntityViewDisplay; + /** * @addtogroup updates-8.0.0-beta * @{ @@ -41,3 +45,18 @@ function system_post_update_recalculate_configuration_entity_dependencies(&$sand /** * @} End of "addtogroup updates-8.0.0-beta". */ + +/** + * Update entity displays to contain the region for each field. + */ +function system_post_update_add_region_to_entity_displays() { + $entity_save = function (EntityDisplayInterface $entity) { + foreach ($entity->getComponents() as $name => $component) { + // setComponent() will fill in the correct region based on the 'type'. + $entity->setComponent($name, $component); + } + $entity->save(); + }; + array_map($entity_save, EntityViewDisplay::loadMultiple()); + array_map($entity_save, EntityFormDisplay::loadMultiple()); +} diff --git a/core/profiles/standard/config/install/core.entity_form_display.block_content.basic.default.yml b/core/profiles/standard/config/install/core.entity_form_display.block_content.basic.default.yml index ee0c138..7ccb5b0 100644 --- a/core/profiles/standard/config/install/core.entity_form_display.block_content.basic.default.yml +++ b/core/profiles/standard/config/install/core.entity_form_display.block_content.basic.default.yml @@ -14,6 +14,7 @@ content: body: type: text_textarea_with_summary weight: -4 + region: content settings: rows: 9 summary_rows: 3 @@ -22,6 +23,7 @@ content: info: type: string_textfield weight: -5 + region: content settings: size: 60 placeholder: '' diff --git a/core/profiles/standard/config/install/core.entity_form_display.comment.comment.default.yml b/core/profiles/standard/config/install/core.entity_form_display.comment.comment.default.yml index fa5d834..1010be2 100644 --- a/core/profiles/standard/config/install/core.entity_form_display.comment.comment.default.yml +++ b/core/profiles/standard/config/install/core.entity_form_display.comment.comment.default.yml @@ -13,9 +13,11 @@ mode: default content: author: weight: -2 + region: content comment_body: type: text_textarea weight: 11 + region: content settings: rows: 5 placeholder: '' @@ -23,6 +25,7 @@ content: subject: type: string_textfield weight: 10 + region: content settings: size: 60 placeholder: '' diff --git a/core/profiles/standard/config/install/core.entity_form_display.node.article.default.yml b/core/profiles/standard/config/install/core.entity_form_display.node.article.default.yml index 79156b2..c94e36e 100644 --- a/core/profiles/standard/config/install/core.entity_form_display.node.article.default.yml +++ b/core/profiles/standard/config/install/core.entity_form_display.node.article.default.yml @@ -21,6 +21,7 @@ content: body: type: text_textarea_with_summary weight: 1 + region: content settings: rows: 9 summary_rows: 3 @@ -29,16 +30,19 @@ content: comment: type: comment_default weight: 20 + region: content settings: { } third_party_settings: { } created: type: datetime_timestamp weight: 10 + region: content settings: { } third_party_settings: { } field_image: type: image_image weight: 4 + region: content settings: progress_indicator: throbber preview_image_style: thumbnail @@ -46,11 +50,13 @@ content: field_tags: type: entity_reference_autocomplete_tags weight: 3 + region: content settings: { } third_party_settings: { } path: type: path weight: 30 + region: content settings: { } third_party_settings: { } promote: @@ -58,16 +64,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: 0 + region: content settings: size: 60 placeholder: '' @@ -75,6 +84,7 @@ content: uid: type: entity_reference_autocomplete weight: 5 + region: content settings: match_operator: CONTAINS size: 60 diff --git a/core/profiles/standard/config/install/core.entity_form_display.node.page.default.yml b/core/profiles/standard/config/install/core.entity_form_display.node.page.default.yml index 1fef06d..0b7ffd1 100644 --- a/core/profiles/standard/config/install/core.entity_form_display.node.page.default.yml +++ b/core/profiles/standard/config/install/core.entity_form_display.node.page.default.yml @@ -15,6 +15,7 @@ content: body: type: text_textarea_with_summary weight: 31 + region: content settings: rows: 9 summary_rows: 3 @@ -23,11 +24,13 @@ content: created: type: datetime_timestamp weight: 10 + region: content settings: { } third_party_settings: { } path: type: path weight: 30 + region: content settings: { } third_party_settings: { } promote: @@ -35,16 +38,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: '' @@ -52,6 +58,7 @@ content: uid: type: entity_reference_autocomplete weight: 5 + region: content settings: match_operator: CONTAINS size: 60 diff --git a/core/profiles/standard/config/install/core.entity_form_display.user.user.default.yml b/core/profiles/standard/config/install/core.entity_form_display.user.user.default.yml index 466b6e0..6832229 100644 --- a/core/profiles/standard/config/install/core.entity_form_display.user.user.default.yml +++ b/core/profiles/standard/config/install/core.entity_form_display.user.user.default.yml @@ -14,12 +14,16 @@ mode: default content: account: weight: -10 + region: content contact: weight: 5 + region: content language: weight: 0 + region: content timezone: weight: 6 + region: content user_picture: type: image_image settings: @@ -27,4 +31,5 @@ content: preview_image_style: thumbnail third_party_settings: { } weight: -1 + region: content hidden: { } diff --git a/core/profiles/standard/config/install/core.entity_view_display.block_content.basic.default.yml b/core/profiles/standard/config/install/core.entity_view_display.block_content.basic.default.yml index bd52f77..e494882 100644 --- a/core/profiles/standard/config/install/core.entity_view_display.block_content.basic.default.yml +++ b/core/profiles/standard/config/install/core.entity_view_display.block_content.basic.default.yml @@ -15,6 +15,7 @@ content: label: hidden type: text_default weight: 0 + region: content settings: { } third_party_settings: { } hidden: { } diff --git a/core/profiles/standard/config/install/core.entity_view_display.comment.comment.default.yml b/core/profiles/standard/config/install/core.entity_view_display.comment.comment.default.yml index 1ed49ce..6ae213d 100644 --- a/core/profiles/standard/config/install/core.entity_view_display.comment.comment.default.yml +++ b/core/profiles/standard/config/install/core.entity_view_display.comment.comment.default.yml @@ -15,8 +15,10 @@ content: label: hidden type: text_default weight: 0 + region: content settings: { } third_party_settings: { } links: weight: 100 + region: content hidden: { } diff --git a/core/profiles/standard/config/install/core.entity_view_display.node.article.default.yml b/core/profiles/standard/config/install/core.entity_view_display.node.article.default.yml index 98a2de8..5c43252 100644 --- a/core/profiles/standard/config/install/core.entity_view_display.node.article.default.yml +++ b/core/profiles/standard/config/install/core.entity_view_display.node.article.default.yml @@ -22,12 +22,14 @@ content: body: type: text_default weight: 0 + region: content settings: { } third_party_settings: { } label: hidden comment: type: comment_default weight: 110 + region: content label: above settings: view_mode: default @@ -36,6 +38,7 @@ content: field_image: type: image weight: -1 + region: content settings: image_style: large image_link: '' @@ -44,12 +47,14 @@ content: field_tags: type: entity_reference_label weight: 10 + region: content label: above settings: link: true third_party_settings: { } links: weight: 100 + region: content hidden: field_image: true field_tags: true diff --git a/core/profiles/standard/config/install/core.entity_view_display.node.article.rss.yml b/core/profiles/standard/config/install/core.entity_view_display.node.article.rss.yml index 75a14a3..84660b6 100644 --- a/core/profiles/standard/config/install/core.entity_view_display.node.article.rss.yml +++ b/core/profiles/standard/config/install/core.entity_view_display.node.article.rss.yml @@ -17,6 +17,7 @@ mode: rss content: links: weight: 100 + region: content hidden: body: true comment: true diff --git a/core/profiles/standard/config/install/core.entity_view_display.node.article.teaser.yml b/core/profiles/standard/config/install/core.entity_view_display.node.article.teaser.yml index 43ee079..7b96908 100644 --- a/core/profiles/standard/config/install/core.entity_view_display.node.article.teaser.yml +++ b/core/profiles/standard/config/install/core.entity_view_display.node.article.teaser.yml @@ -21,6 +21,7 @@ content: body: type: text_summary_or_trimmed weight: 0 + region: content settings: trim_length: 600 third_party_settings: { } @@ -28,6 +29,7 @@ content: field_image: type: image weight: -1 + region: content settings: image_style: medium image_link: content @@ -36,12 +38,14 @@ content: field_tags: type: entity_reference_label weight: 10 + region: content settings: link: true third_party_settings: { } label: above links: weight: 100 + region: content hidden: comment: true field_image: true diff --git a/core/profiles/standard/config/install/core.entity_view_display.node.page.default.yml b/core/profiles/standard/config/install/core.entity_view_display.node.page.default.yml index dcb2d3e..8afd942 100644 --- a/core/profiles/standard/config/install/core.entity_view_display.node.page.default.yml +++ b/core/profiles/standard/config/install/core.entity_view_display.node.page.default.yml @@ -16,8 +16,10 @@ content: label: hidden type: text_default weight: 100 + region: content settings: { } third_party_settings: { } links: weight: 101 + region: content hidden: { } diff --git a/core/profiles/standard/config/install/core.entity_view_display.node.page.teaser.yml b/core/profiles/standard/config/install/core.entity_view_display.node.page.teaser.yml index f235a10..bc7a68c 100644 --- a/core/profiles/standard/config/install/core.entity_view_display.node.page.teaser.yml +++ b/core/profiles/standard/config/install/core.entity_view_display.node.page.teaser.yml @@ -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: { } diff --git a/core/profiles/standard/config/install/core.entity_view_display.user.user.compact.yml b/core/profiles/standard/config/install/core.entity_view_display.user.user.compact.yml index 4c13792..2ff13ad 100644 --- a/core/profiles/standard/config/install/core.entity_view_display.user.user.compact.yml +++ b/core/profiles/standard/config/install/core.entity_view_display.user.user.compact.yml @@ -16,6 +16,7 @@ content: user_picture: type: image weight: 0 + region: content settings: image_style: thumbnail image_link: content diff --git a/core/profiles/standard/config/install/core.entity_view_display.user.user.default.yml b/core/profiles/standard/config/install/core.entity_view_display.user.user.default.yml index 9e4621d..ef1fdd7 100644 --- a/core/profiles/standard/config/install/core.entity_view_display.user.user.default.yml +++ b/core/profiles/standard/config/install/core.entity_view_display.user.user.default.yml @@ -14,9 +14,11 @@ mode: default content: member_for: weight: 5 + region: content user_picture: type: image weight: 0 + region: content settings: image_style: thumbnail image_link: content diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityDisplayFormBaseTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityDisplayFormBaseTest.php new file mode 100644 index 0000000..09124c0 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityDisplayFormBaseTest.php @@ -0,0 +1,296 @@ +prophesize(EntityDisplayInterface::class); + $entity->getPluginCollections()->willReturn([]); + + // A field with no initial values, with mismatched submitted values, type is + // hidden. + $entity->getComponent('new_field_mismatch_type_hidden')->willReturn([]); + $field_values['new_field_mismatch_type_hidden'] = [ + 'weight' => 0, + 'type' => 'hidden', + 'region' => 'content', + ]; + $entity + ->setComponent('new_field_mismatch_type_hidden', [ + 'weight' => 0, + 'region' => 'content', + ]) + ->will(function($args) { + // On subsequent calls, getComponent() will return the newly set values, + // plus the updated type value. + $args[1] += ['type' => 'textfield']; + $this->getComponent($args[0])->willReturn($args[1]); + $this->setComponent($args[0], $args[1])->shouldBeCalled(); + }) + ->shouldBeCalled(); + + // A field with no initial values, with mismatched submitted values, type is + // visible. + $entity->getComponent('new_field_mismatch_type_visible')->willReturn([]); + $field_values['new_field_mismatch_type_visible'] = [ + 'weight' => 0, + 'type' => 'textfield', + 'region' => 'hidden', + ]; + $entity + ->setComponent('new_field_mismatch_type_visible', [ + 'weight' => 0, + 'type' => 'textfield', + ]) + ->will(function($args) { + // On subsequent calls, getComponent() will return the newly set values, + // plus the updated region value. + $args[1] += ['region' => 'content']; + $this->getComponent($args[0])->willReturn($args[1]); + $this->setComponent($args[0], $args[1])->shouldBeCalled(); + }) + ->shouldBeCalled(); + + // An initially hidden field, with identical submitted values. + $entity->getComponent('field_hidden_no_changes') + ->willReturn([ + 'weight' => 0, + 'type' => 'hidden', + 'region' => 'hidden', + ]); + $field_values['field_hidden_no_changes'] = [ + 'weight' => 0, + 'type' => 'hidden', + 'region' => 'hidden', + ]; + $entity->removeComponent('field_hidden_no_changes') + ->will(function ($args) { + // On subsequent calls, getComponent() will return an empty array. + $this->getComponent($args[0])->willReturn([]); + }) + ->shouldBeCalled(); + + // An initially visible field, with identical submitted values. + $entity->getComponent('field_visible_no_changes') + ->willReturn([ + 'weight' => 0, + 'type' => 'textfield', + 'region' => 'content', + ]); + $field_values['field_visible_no_changes'] = [ + 'weight' => 0, + 'type' => 'textfield', + 'region' => 'content', + ]; + $entity + ->setComponent('field_visible_no_changes', [ + 'weight' => 0, + 'type' => 'textfield', + 'region' => 'content', + ]) + ->shouldBeCalled(); + + // An initially hidden field, with a submitted type change. + $entity->getComponent('field_start_hidden_change_type') + ->willReturn([ + 'weight' => 0, + 'type' => 'hidden', + 'region' => 'hidden', + ]); + $field_values['field_start_hidden_change_type'] = [ + 'weight' => 0, + 'type' => 'textfield', + 'region' => 'hidden', + ]; + $entity + ->setComponent('field_start_hidden_change_type', [ + 'weight' => 0, + 'type' => 'textfield', + ]) + ->will(function($args) { + // On subsequent calls, getComponent() will return the newly set values, + // plus the updated region value. + $args[1] += ['region' => 'content']; + $this->getComponent($args[0])->willReturn($args[1]); + $this->setComponent($args[0], $args[1])->shouldBeCalled(); + }) + ->shouldBeCalled(); + + // An initially hidden field, with a submitted region change. + $entity->getComponent('field_start_hidden_change_region') + ->willReturn([ + 'weight' => 0, + 'type' => 'hidden', + 'region' => 'hidden', + ]); + $field_values['field_start_hidden_change_region'] = [ + 'weight' => 0, + 'type' => 'hidden', + 'region' => 'content', + ]; + $entity + ->setComponent('field_start_hidden_change_region', [ + 'weight' => 0, + 'region' => 'content', + ]) + ->will(function($args) { + // On subsequent calls, getComponent() will return the newly set values, + // plus the updated type value. + $args[1] += ['type' => 'textfield']; + $this->getComponent($args[0])->willReturn($args[1]); + $this->setComponent($args[0], $args[1])->shouldBeCalled(); + }) + ->shouldBeCalled(); + + // An initially hidden field, with a submitted region and type change. + $entity->getComponent('field_start_hidden_change_both') + ->willReturn([ + 'weight' => 0, + 'type' => 'hidden', + 'region' => 'hidden', + ]); + $field_values['field_start_hidden_change_both'] = [ + 'weight' => 0, + 'type' => 'textfield', + 'region' => 'content', + ]; + $entity + ->setComponent('field_start_hidden_change_both', [ + 'weight' => 0, + 'type' => 'textfield', + 'region' => 'content', + ]) + ->will(function($args) { + // On subsequent calls, getComponent() will return the newly set values. + $this->getComponent($args[0])->willReturn($args[1]); + }) + ->shouldBeCalled(); + + // An initially visible field, with a submitted type change. + $entity->getComponent('field_start_visible_change_type') + ->willReturn([ + 'weight' => 0, + 'type' => 'textfield', + 'region' => 'content', + ]); + $field_values['field_start_visible_change_type'] = [ + 'weight' => 0, + 'type' => 'hidden', + 'region' => 'content', + ]; + $entity->removeComponent('field_start_visible_change_type') + ->will(function ($args) { + // On subsequent calls, getComponent() will return an empty array. + $this->getComponent($args[0])->willReturn([]); + }) + ->shouldBeCalled(); + + // An initially visible field, with a submitted region change. + $entity->getComponent('field_start_visible_change_region') + ->willReturn([ + 'weight' => 0, + 'type' => 'textfield', + 'region' => 'content', + ]); + $field_values['field_start_visible_change_region'] = [ + 'weight' => 0, + 'type' => 'textfield', + 'region' => 'hidden', + ]; + $entity->removeComponent('field_start_visible_change_region') + ->will(function ($args) { + // On subsequent calls, getComponent() will return an empty array. + $this->getComponent($args[0])->willReturn([]); + }) + ->shouldBeCalled(); + + // An initially visible field, with a submitted region and type change. + $entity->getComponent('field_start_visible_change_both') + ->willReturn([ + 'weight' => 0, + 'type' => 'textfield', + 'region' => 'content', + ]); + $field_values['field_start_visible_change_both'] = [ + 'weight' => 0, + 'type' => 'hidden', + 'region' => 'hidden', + ]; + $entity->removeComponent('field_start_visible_change_both') + ->will(function ($args) { + // On subsequent calls, getComponent() will return an empty array. + $this->getComponent($args[0])->willReturn([]); + }) + ->shouldBeCalled(); + + // A field that is flagged for plugin settings update on the second build. + $entity->getComponent('field_plugin_settings_update') + ->willReturn([ + 'weight' => 0, + 'type' => 'textfield', + 'region' => 'content', + ]); + $field_values['field_plugin_settings_update'] = [ + 'weight' => 0, + 'type' => 'textfield', + 'region' => 'content', + 'settings_edit_form' => [ + 'third_party_settings' => [ + 'foo' => 'bar', + ], + ], + ]; + $entity + ->setComponent('field_plugin_settings_update', [ + 'weight' => 0, + 'type' => 'textfield', + 'region' => 'content', + ]) + ->will(function ($args) { + // On subsequent calls, getComponent() will return the newly set values. + $this->getComponent($args[0])->willReturn($args[1]); + $args[1] += [ + 'settings' => [], + 'third_party_settings' => [ + 'foo' => 'bar', + ], + ]; + $this->setComponent($args[0], $args[1])->shouldBeCalled(); + }) + ->shouldBeCalled(); + + $form_object = new EntityViewDisplayEditForm($this->container->get('plugin.manager.field.field_type'), $this->container->get('plugin.manager.field.formatter')); + $form_object->setEntity($entity->reveal()); + + $form = [ + '#fields' => array_keys($field_values), + '#extra' => [], + ]; + $form_state = new FormState(); + $form_state->setValues(['fields' => $field_values]); + + $form_object->buildEntity($form, $form_state); + + // Flag one field for updating plugin settings. + $form_state->set('plugin_settings_update', 'field_plugin_settings_update'); + // During form submission, buildEntity() will be called twice. Simulate that + // here to prove copyFormValuesToEntity() is idempotent. + $form_object->buildEntity($form, $form_state); + } + +}