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/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..b328a75 --- /dev/null +++ b/core/modules/field_layout/config/schema/field_layout.schema.yml @@ -0,0 +1,23 @@ +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' + fields: + type: sequence + label: 'Fields' + sequence: + type: mapping + label: 'Field' + mapping: + region: + type: string + label: 'Region' 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..c0fbe3f --- /dev/null +++ b/core/modules/field_layout/field_layout.install @@ -0,0 +1,24 @@ +save(); + } + + // 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..59ed060 --- /dev/null +++ b/core/modules/field_layout/field_layout.libraries.yml @@ -0,0 +1,6 @@ +drupal.field_layout: + version: VERSION + js: + js/field_layout.js: {} + dependencies: + - field_ui/drupal.field_ui diff --git a/core/modules/field_layout/field_layout.module b/core/modules/field_layout/field_layout.module new file mode 100644 index 0000000..b62c89a --- /dev/null +++ b/core/modules/field_layout/field_layout.module @@ -0,0 +1,126 @@ +' . 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[] */ + 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 and + // initialize the field layout based on default configuration. + if ($entity instanceof EntityDisplayInterface && !$entity->getThirdPartySettings('field_layout')) { + $field_layout = []; + $field_layout += array_fill_keys(array_keys($entity->get('content')), ['region' => 'content']); + $field_layout += array_fill_keys(array_keys($entity->get('hidden')), ['region' => 'hidden']); + $entity->setThirdPartySetting('field_layout', 'fields', $field_layout); + $entity->setThirdPartySetting('field_layout', 'layout', 'default'); + } +} + +/** + * Implements hook_entity_view_alter(). + */ +function field_layout_entity_view_alter(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display) { + _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) { + _field_layout_apply_layout($form, $form_object->getFormDisplay($form_state)); + } +} + +/** + * Applies the layout to an entity build. + * + * @param array $build + * A renderable array representing the entity content or form. + * @param \Drupal\Core\Entity\Display\EntityDisplayInterface $display + * The entity display holding the display options configured for the + * entity components. + */ +function _field_layout_apply_layout(array &$build, EntityDisplayInterface $display) { + if (!$layout_definition = \Drupal::service('field_layout.layout_repository')->getLayoutForDisplay($display)) { + return; + } + + // Add the regions to the $build in the correct order. + foreach ($layout_definition['regions'] as $region => $region_info) { + $build['field_layout__' . $region]['#theme_wrappers'][] = 'field_layout_region'; + $build['field_layout__' . $region]['#region'] = $region; + } + + // Move the field from the top-level of $build into a region-specific section. + foreach ($display->getThirdPartySetting('field_layout', 'fields', []) as $name => $field) { + if (isset($build[$name]) && $field['region'] !== 'hidden') { + $build['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', + ], + ]; +} + +/** + * 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/js/field_layout.js b/core/modules/field_layout/js/field_layout.js new file mode 100644 index 0000000..9b8ab45 --- /dev/null +++ b/core/modules/field_layout/js/field_layout.js @@ -0,0 +1,76 @@ +/** + * @file + * Attaches the behaviors for the Field Layout module. + */ +(function ($, Drupal) { + + "use strict"; + + /** + * Row handlers for the 'Manage display' screen. + */ + Drupal.fieldUIDisplayOverview = Drupal.fieldUIDisplayOverview || {}; + + Drupal.fieldUIDisplayOverview.field_layout = function (row, data) { + this.row = row; + this.name = data.name; + this.region = data.region; + this.tableDrag = data.tableDrag; + + // Attach change listener to the 'region' select. + this.$regionSelect = $(row).find('select.field-layout-region'); + this.$regionSelect.on('change', Drupal.fieldUIOverview.onChange); + + // Load the Field UI row handler. + this.fieldRowHandler = new Drupal.fieldUIDisplayOverview.field(row, data); + + return this; + }; + + Drupal.fieldUIDisplayOverview.field_layout.prototype = { + + /** + * Returns the region corresponding to the current form values of the row. + * + * @return {string} + * Either 'hidden' or 'content'. + */ + getRegion: function () { + return this.$regionSelect.val(); + }, + + /** + * Reacts to a row being changed regions. + * + * This function is called when the row is moved to a different region, as + * a + * result of either : + * - a drag-and-drop action (the row's form elements then probably need to + * be updated accordingly) + * - user input in one of the form elements watched by the + * {@link Drupal.fieldUIOverview.onChange} change listener. + * + * @param {string} region + * The name of the new region for the row. + * + * @return {object} + * A hash object indicating which rows should be Ajax-updated as a result + * of the change, in the format expected by + * {@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); + + // Call the field UI row handler implementation. + // @todo Remove the massaging of region once + // Drupal.fieldUIDisplayOverview.field.regionChange is fixed in + // https://www.drupal.org/node/2796885. + return this.fieldRowHandler.regionChange(region === 'hidden' || 'content'); + } + }; + +})(jQuery, Drupal); 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..b769bee --- /dev/null +++ b/core/modules/field_layout/src/Form/FieldLayoutEntityDisplayFormTrait.php @@ -0,0 +1,176 @@ +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['#attached']['library'][] = 'field_layout/drupal.field_layout'; + + $form['fields']['#tabledrag'][] = [ + 'action' => 'match', + 'relationship' => 'parent', + 'group' => 'field-layout-region', + 'subgroup' => 'field-layout-region', + 'source' => 'field-name', + ]; + + $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()->getThirdPartySetting('field_layout', 'layout', 'default'), + ]; + + return $form; + } + + /** + * Overrides \Drupal\field_ui\Form\EntityDisplayFormBase::getRowRegion(). + */ + public function getRowRegion($row) { + switch ($row['#row_type']) { + case 'field': + case 'extra_field': + return $row['region']['#value'] ?: 'hidden'; + } + } + + /** + * 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 ThirdPartySettingsInterface) { + $old_layout = $entity->getThirdPartySetting('field_layout', 'layout'); + $new_layout = $form_state->getValue('field_layout'); + $fields = []; + foreach ($form_state->getValue('fields') as $field_name => $values) { + // If the layout is changing, reset all fields. + // @todo Devise a mechanism for mapping old regions to new ones in + // https://www.drupal.org/node/2796877. + $fields[$field_name]['region'] = $old_layout === $new_layout ? $values['region'] : 'hidden'; + } + $entity->setThirdPartySetting('field_layout', 'fields', $fields); + $entity->setThirdPartySetting('field_layout', 'layout', $new_layout); + } + } + + /** + * Adds a column with the Region select. + * + * @param array $row + * A table row array. + * @param string $field_name + * The field name. + * @param string $label + * The field name. + * + * @return array + * A table row array. + */ + protected function addRegionSelect(array $row, $field_name, $label) { + $row['#js_settings']['rowHandler'] = 'field_layout'; + $field_layout = $this->getEntity()->getThirdPartySetting('field_layout', 'fields', []); + $new_row = [ + 'region' => [ + '#type' => 'select', + '#title' => $this->t('Region for @title', ['@title' => $label]), + '#title_display' => 'invisible', + '#options' => $this->getRegionOptions(), + '#empty_value' => 'hidden', + '#default_value' => isset($field_layout[$field_name]['region']) ? $field_layout[$field_name]['region'] : 'hidden', + '#attributes' => ['class' => ['field-layout-region']], + ], + ]; + + $position = array_search('parent_wrapper', array_keys($row)) + 1; + return array_merge(array_slice($row, 0, $position), $new_row, array_slice($row, $position)); + } + + /** + * Adjusts the table header for field layout. + * + * @param array $header + * The table header. + * + * @return array + * The table header. + */ + protected function adjustTableHeader(array $header) { + array_splice($header, 3, 0, [$this->t('Region')]); + return $header; + } + + /** + * Gets the form entity. + * + * @return \Drupal\Core\Entity\Display\EntityDisplayInterface + * 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..3579155 --- /dev/null +++ b/core/modules/field_layout/src/Form/FieldLayoutEntityFormDisplayEditForm.php @@ -0,0 +1,78 @@ +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); + } + + /** + * {@inheritdoc} + */ + protected function buildFieldRow(FieldDefinitionInterface $field_definition, array $form, FormStateInterface $form_state) { + $row = parent::buildFieldRow($field_definition, $form, $form_state); + return $this->addRegionSelect($row, $field_definition->getName(), $field_definition->getLabel()); + } + + /** + * {@inheritdoc} + */ + protected function buildExtraFieldRow($field_id, $extra_field) { + $row = parent::buildExtraFieldRow($field_id, $extra_field); + return $this->addRegionSelect($row, $field_id, $extra_field['label']); + } + + /** + * {@inheritdoc} + */ + protected function getTableHeader() { + $header = parent::getTableHeader(); + return $this->adjustTableHeader($header); + } + +} 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..c36fc1d --- /dev/null +++ b/core/modules/field_layout/src/Form/FieldLayoutEntityViewDisplayEditForm.php @@ -0,0 +1,78 @@ +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); + } + + /** + * {@inheritdoc} + */ + protected function buildFieldRow(FieldDefinitionInterface $field_definition, array $form, FormStateInterface $form_state) { + $row = parent::buildFieldRow($field_definition, $form, $form_state); + return $this->addRegionSelect($row, $field_definition->getName(), $field_definition->getLabel()); + } + + /** + * {@inheritdoc} + */ + protected function buildExtraFieldRow($field_id, $extra_field) { + $row = parent::buildExtraFieldRow($field_id, $extra_field); + return $this->addRegionSelect($row, $field_id, $extra_field['label']); + } + + /** + * {@inheritdoc} + */ + protected function getTableHeader() { + $header = parent::getTableHeader(); + return $this->adjustTableHeader($header); + } + +} diff --git a/core/modules/field_layout/src/LayoutRepository.php b/core/modules/field_layout/src/LayoutRepository.php new file mode 100644 index 0000000..83b3108 --- /dev/null +++ b/core/modules/field_layout/src/LayoutRepository.php @@ -0,0 +1,79 @@ +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'), + ], + ], + ], + '1col_stacked' => [ + 'label' => $this->t('One column stacked'), + 'regions' => [ + 'top' => [ + 'label' => $this->t('Top'), + ], + 'bottom' => [ + 'label' => $this->t('Bottom'), + ], + ], + ], + '2col' => [ + 'label' => $this->t('Two column'), + 'regions' => [ + 'left' => [ + 'label' => $this->t('Left'), + ], + 'right' => [ + 'label' => $this->t('Right'), + ], + ], + ], + ]; + return $layouts; + } + + /** + * {@inheritdoc} + */ + public function getLayoutForDisplay(EntityDisplayInterface $display) { + $layout_name = $display->getThirdPartySetting('field_layout', 'layout', 'default'); + $layout_definitions = $this->getLayoutDefinitions(); + if (!$layout_name || !isset($layout_definitions[$layout_name])) { + return []; + } + + return $layout_definitions[$layout_name]; + } + +} diff --git a/core/modules/field_layout/src/LayoutRepositoryInterface.php b/core/modules/field_layout/src/LayoutRepositoryInterface.php new file mode 100644 index 0000000..f8287f3 --- /dev/null +++ b/core/modules/field_layout/src/LayoutRepositoryInterface.php @@ -0,0 +1,31 @@ +. + * - region: The name of the region variable as defined in the theme's + * .info.yml file. + * + * @see template_preprocess_region() + * + * @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..ea007e0 --- /dev/null +++ b/core/modules/field_layout/tests/src/FunctionalJavascript/FieldLayoutTest.php @@ -0,0 +1,249 @@ +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', '2col'); + $this->submitForm([], 'Save'); + + $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->assertSession()->elementNotExists('css', '.field-layout-region'); + $this->assertSession()->fieldNotExists('field_test_text[0][value]'); + + // 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 left region. + $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(); + $field_test_text_row->hasClass('.drag-previous'); + $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--left .field--name-field-test-text'); + $this->assertFieldInRegion('field_test_text[0][value]', 'left'); + + // 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', '2col'); + $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-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(); + $field_test_text_row->hasClass('.drag-previous'); + $this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][type]', 'text_trimmed')->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-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-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-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->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..303ce6c --- /dev/null +++ b/core/modules/field_layout/tests/src/Unit/LayoutRepositoryTest.php @@ -0,0 +1,59 @@ +getStringTranslationStub()); + + $expected = ['default', '1col_stacked', '2col']; + + $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(EntityDisplayInterface::class); + $display->getThirdPartySetting('field_layout', 'layout', 'default')->willReturn('2col'); + + $expected = ['left', 'right']; + + $layout_definition = $layout_repository->getLayoutForDisplay($display->reveal()); + $this->assertEquals($expected, array_keys($layout_definition['regions'])); + } + + /** + * @covers ::getLayoutForDisplay + */ + public function testGetLayoutForDisplayEmpty() { + $layout_repository = new LayoutRepository($this->getStringTranslationStub()); + + $display = $this->prophesize(EntityDisplayInterface::class); + $display->getThirdPartySetting('field_layout', 'layout', 'default')->willReturn('default'); + + $expected = ['content']; + + $layout_definition = $layout_repository->getLayoutForDisplay($display->reveal()); + $this->assertEquals($expected, array_keys($layout_definition['regions'])); + } + +}