diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/EntityReferenceEntityFormatter.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/EntityReferenceEntityFormatter.php index a40b56a..7f4d393 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/EntityReferenceEntityFormatter.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/EntityReferenceEntityFormatter.php @@ -2,13 +2,17 @@ namespace Drupal\Core\Field\Plugin\Field\FieldFormatter; +use Drupal\Core\Entity\Entity\EntityViewMode; use Drupal\Core\Entity\EntityDisplayRepositoryInterface; +use Drupal\Core\Entity\EntityTypeBundleInfoInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Logger\LoggerChannelFactoryInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\Render\Markup; +use Drupal\Core\Url; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -54,6 +58,13 @@ class EntityReferenceEntityFormatter extends EntityReferenceFormatterBase implem protected $entityDisplayRepository; /** + * The entity type bundle info. + * + * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface + */ + protected $entityTypeBundleInfo; + + /** * An array of counters for the recursive rendering protection. * * Each counter takes into account all the relevant information about the @@ -88,12 +99,15 @@ class EntityReferenceEntityFormatter extends EntityReferenceFormatterBase implem * The entity type manager. * @param \Drupal\Core\Entity\EntityDisplayRepositoryInterface $entity_display_repository * The entity display repository. + * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info + * The entity type bundle info. */ - public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, LoggerChannelFactoryInterface $logger_factory, EntityTypeManagerInterface $entity_type_manager, EntityDisplayRepositoryInterface $entity_display_repository) { + public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, LoggerChannelFactoryInterface $logger_factory, EntityTypeManagerInterface $entity_type_manager, EntityDisplayRepositoryInterface $entity_display_repository, EntityTypeBundleInfoInterface $entity_type_bundle_info) { parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings); $this->loggerFactory = $logger_factory; $this->entityTypeManager = $entity_type_manager; $this->entityDisplayRepository = $entity_display_repository; + $this->entityTypeBundleInfo = $entity_type_bundle_info; } /** @@ -110,7 +124,8 @@ public static function create(ContainerInterface $container, array $configuratio $configuration['third_party_settings'], $container->get('logger.factory'), $container->get('entity_type.manager'), - $container->get('entity_display.repository') + $container->get('entity_display.repository'), + $container->get('entity_type.bundle.info') ); } @@ -125,15 +140,158 @@ public static function defaultSettings() { } /** + * Wrapper around ::getSetting() to reflect current values from Form State. + * + * @param string $key + * The setting name. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * (optional) The form state object. If absent, this method is equivalent to + * parent::getSetting(). + * + * @return mixed|null + * The value of the setting, or NULL if absent. + */ + public function getSetting($key, FormStateInterface $form_state = NULL) { + if ($form_state) { + $field_name = $this->fieldDefinition->getName(); + $form_state_key = [ + 'fields', + $field_name, + 'settings_edit_form', + 'settings', + $key, + ]; + if ($form_state->hasValue($form_state_key)) { + return $form_state->getValue($form_state_key); + } + } + + return parent::getSetting($key); + } + + /** + * Ajax callback for view mode selection change. + */ + public static function onViewModeChange(array &$form, FormStateInterface $form_state) { + return $form['fields'][$form_state->get('plugin_settings_edit')]['plugin']['settings_edit_form']['settings']; + } + + /** + * Rebuilds the form on select submit. + */ + public static function rebuildOnSubmit(array &$form, FormStateInterface $form_state) { + $form_state->setRebuild(TRUE); + } + + /** * {@inheritdoc} */ public function settingsForm(array $form, FormStateInterface $form_state) { + $entity_type_id = $this->getFieldSetting('target_type'); + $entity_type_label = $this->entityTypeManager->getDefinition($entity_type_id)->getLabel(); + $bundle_entity_type_id = $this->entityTypeManager->getDefinition($entity_type_id)->getBundleEntityType(); + $target_bundles_setting = $this->getFieldSetting('handler_settings')['target_bundles']; + if ($target_bundles_setting === NULL) { + // If no bundle is configured in the field settings, all are selectable. + $bundles = array_keys($this->entityTypeBundleInfo->getBundleInfo($entity_type_id)); + } + else { + $bundles = array_keys($target_bundles_setting); + } + + $currently_selected_view_mode_name = $this->getSetting('view_mode', $form_state); + $currently_selected_view_mode = EntityViewMode::load("$entity_type_id.$currently_selected_view_mode_name"); + // @todo Simplify this in https://www.drupal.org/node/2844203. + $currently_selected_view_mode_label = $currently_selected_view_mode ? $currently_selected_view_mode->label() : $this->t('Default'); + + $elements['#prefix'] = '
'; + $elements['#suffix'] = '
'; + $description = [ + '#theme' => 'item_list', + '#context' => ['list_style' => 'comma-list'], + '#items' => [], + ]; + // If targeting entities of the same type this field is attached to, skip + // the viewmode for the bundle we are currently editing, once it wouldn't + // make sense to open in the modal the same form we have on the main page. + if (count($bundles) === 1 && !($entity_type_id === $this->fieldDefinition->getTargetEntityTypeId() + && $bundles[0] === $form_state->getFormObject()->getEntity()->getTargetBundle() + && $currently_selected_view_mode_name === $this->viewMode)) { + $description['#items'][] = [ + '#type' => 'link', + '#title' => $this->t('Configure %view-mode-label view mode', ['%view-mode-label' => $currently_selected_view_mode_label]), + '#url' => $currently_selected_view_mode_name === 'default' + ? Url::fromRoute(sprintf('entity.entity_view_display.%s.default', $entity_type_id), [$bundle_entity_type_id => $bundles[0]]) + : Url::fromRoute(sprintf('entity.entity_view_display.%s.view_mode', $entity_type_id), [$bundle_entity_type_id => $bundles[0], 'view_mode_name' => $currently_selected_view_mode_name]), + '#attributes' => [ + 'class' => ['use-ajax'], + 'data-dialog-type' => 'modal', + 'data-dialog-options' => json_encode([ + 'width' => 800, + ]), + ], + ]; + } + else { + $bundle_info = entity_get_bundles($entity_type_id); + for ($i = 0; $i < count($bundles); $i++) { + $skip_bundle = $entity_type_id === $this->fieldDefinition->getTargetEntityTypeId() + && $bundles[$i] === $form_state->getFormObject()->getEntity()->getTargetBundle() + && $currently_selected_view_mode_name === $this->viewMode; + if ($skip_bundle) { + continue; + } + $description['#items'][] = [ + '#type' => 'link', + '#title' => $this->t('%bundle-label %entity-type-label', ['%bundle-label' => $bundle_info[$bundles[$i]]['label'], '%entity-type-label' => $entity_type_label]), + '#url' => $currently_selected_view_mode_name === 'default' + ? Url::fromRoute(sprintf('entity.entity_view_display.%s.default', $entity_type_id), [$bundle_entity_type_id => $bundles[$i]]) + : Url::fromRoute(sprintf('entity.entity_view_display.%s.view_mode', $entity_type_id), [$bundle_entity_type_id => $bundles[$i], 'view_mode_name' => $currently_selected_view_mode_name]), + '#attributes' => [ + 'class' => ['use-ajax'], + 'data-dialog-type' => 'modal', + 'data-dialog-options' => json_encode([ + 'width' => 800, + ]), + ], + ]; + } + if (!empty($description['#items'][0])) { + $description['#items'][0]['#prefix'] = $this->t('Configure %view-mode-label view mode for', [ + '%view-mode-label' => $currently_selected_view_mode_label, + ]) . ' '; + } + } + + $description['#items'][] = [ + '#type' => 'link', + '#title' => !empty($description['#items']) ? $this->t('add new view mode') : $this->t('Add new view mode'), + '#prefix' => !empty($description['#items']) ? Markup::create(t('or') . ' ') : '', + '#url' => Url::fromRoute('entity.entity_view_mode.add_form', ['entity_type_id' => $entity_type_id]), + '#attributes' => [ + 'class' => ['use-ajax'], + 'data-dialog-type' => 'modal', + 'data-dialog-options' => json_encode([ + 'width' => 800, + ]), + ], + ]; $elements['view_mode'] = [ '#type' => 'select', - '#options' => $this->entityDisplayRepository->getViewModeOptions($this->getFieldSetting('target_type')), + '#options' => $this->entityDisplayRepository->getViewModeOptions($entity_type_id), '#title' => t('View mode'), - '#default_value' => $this->getSetting('view_mode'), + '#default_value' => $currently_selected_view_mode_name, + '#ajax' => [ + 'callback' => [static::class, 'onViewModeChange'], + 'wrapper' => 'entity-reference-entity-formatter-settings-ajax', + 'method' => 'replace', + ], + '#submit' => [[static::class, 'rebuildOnSubmit']], + '#executes_submit_callback' => TRUE, '#required' => TRUE, + '#description' => $description + [ + '#access' => \Drupal::currentUser()->hasPermission('administer ' . $entity_type_id . ' display'), + ], ]; return $elements; diff --git a/core/misc/ajax.es6.js b/core/misc/ajax.es6.js index 095154a..1bfcb80 100644 --- a/core/misc/ajax.es6.js +++ b/core/misc/ajax.es6.js @@ -380,6 +380,12 @@ * @type {string} */ this.wrapper = `#${this.wrapper}`; + // To avoid problems when the same form exists multiple times on the same page, allow its + // wrapper to be scoped to the form it lives in. This can f.e. happen when the same base form + // is both in the parent page and in a modal. + if (this.scope && this.scope === 'form') { + this.wrapper = `#${element.form.id} ${this.wrapper}`; + } } /** diff --git a/core/misc/ajax.js b/core/misc/ajax.js index 8ec175f..b806378 100644 --- a/core/misc/ajax.js +++ b/core/misc/ajax.js @@ -168,6 +168,10 @@ function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr if (this.wrapper) { this.wrapper = '#' + this.wrapper; + + if (this.scope && this.scope === 'form') { + this.wrapper = '#' + element.form.id + ' ' + this.wrapper; + } } this.element = element; diff --git a/core/modules/field_ui/src/Controller/EntityDisplayModeController.php b/core/modules/field_ui/src/Controller/EntityDisplayModeController.php index b4a608a..763a025 100644 --- a/core/modules/field_ui/src/Controller/EntityDisplayModeController.php +++ b/core/modules/field_ui/src/Controller/EntityDisplayModeController.php @@ -3,6 +3,7 @@ namespace Drupal\field_ui\Controller; use Drupal\Core\Controller\ControllerBase; +use Drupal\Core\Entity\Entity\EntityViewMode; use Drupal\Core\Url; /** @@ -10,6 +11,26 @@ */ class EntityDisplayModeController extends ControllerBase { + public function editViewDisplayTitle($entity_type_id, $bundle, $view_mode_name) { + $entity_type = $this->entityTypeManager()->getDefinition($entity_type_id); + if ($entity_type->hasKey('bundle')) { + return $this->t('Edit %view-mode-label display of %bundle-label %entity-type-label', [ + '%view-mode-label' => $view_mode_name === 'default' + ? t('Default') + : EntityViewMode::load("$entity_type_id.$view_mode_name")->label(), + '%bundle-label' => $this->entityManager()->getBundleInfo($entity_type_id)[$bundle]['label'], + '%entity-type-label' => $this->entityTypeManager()->getDefinition($entity_type_id)->getPluralLabel(), + ]); + } + else { + return $this->t('Edit %view-mode-label display of %entity-type-label', [ + '%view-mode-label' => $view_mode_name === 'default' + ? t('Default') + : EntityViewMode::load("$entity_type_id.$view_mode_name")->label(), + '%entity-type-label' => $this->entityTypeManager()->getDefinition($entity_type_id)->getPluralLabel(), + ]); + } + } /** * Provides a list of eligible entity types for adding view modes. * diff --git a/core/modules/field_ui/src/Form/EntityDisplayFormBase.php b/core/modules/field_ui/src/Form/EntityDisplayFormBase.php index 06eed1f..f524687 100644 --- a/core/modules/field_ui/src/Form/EntityDisplayFormBase.php +++ b/core/modules/field_ui/src/Form/EntityDisplayFormBase.php @@ -4,6 +4,9 @@ use Drupal\Component\Plugin\Factory\DefaultFactory; use Drupal\Component\Plugin\PluginManagerBase; +use Drupal\Core\Ajax\AjaxResponse; +use Drupal\Core\Ajax\CloseModalDialogCommand; +use Drupal\Core\Ajax\HtmlCommand; use Drupal\Core\Entity\EntityForm; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityWithPluginCollectionInterface; @@ -235,6 +238,7 @@ public function form(array $form, FormStateInterface $form_state) { '#submit' => ['::multistepSubmit'], '#ajax' => [ 'callback' => '::multistepAjax', + 'scope' => 'form', 'wrapper' => 'field-display-overview-wrapper', 'effect' => 'fade', // The button stays hidden, so we hide the Ajax spinner too. Ad-hoc @@ -244,19 +248,47 @@ public function form(array $form, FormStateInterface $form_state) { '#attributes' => ['class' => ['visually-hidden']] ]; - $form['actions'] = ['#type' => 'actions']; - $form['actions']['submit'] = [ - '#type' => 'submit', - '#button_type' => 'primary', - '#value' => $this->t('Save'), - ]; - $form['#attached']['library'][] = 'field_ui/drupal.field_ui'; return $form; } /** + * {@inheritdoc} + */ + protected function actions(array $form, FormStateInterface $form_state) { + $actions = parent::actions($form, $form_state); + $actions['submit']['#ajax'] = [ + 'callback' => '::ajaxCallback', + 'event' => 'click', + ]; + return $actions; + } + + /** + * Ajax callback to close the modal and update the selected text. + * + * @return \Drupal\Core\Ajax\AjaxResponse + * An ajax response object. + */ + public function ajaxCallback(array $form, FormStateInterface $form_state) { + $response = new AjaxResponse(); + if ($form_state->getErrors()) { + unset($form['#prefix'], $form['#suffix']); + $form['status_messages'] = [ + '#type' => 'status_messages', + '#weight' => -10, + ]; + $response->addCommand(new HtmlCommand('#drupal-modal', $form)); + } + else { + // @todo show message for successful saving of settings once https://www.drupal.org/node/77245 lands. This message is set by \Drupal\field_ui\Form\EntityDisplayFormBase::submitForm(). + $response->addCommand(new CloseModalDialogCommand()); + } + return $response; + } + + /** * Builds the table row structure for a single field. * * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition @@ -347,6 +379,7 @@ protected function buildFieldRow(FieldDefinitionInterface $field_definition, arr '#submit' => ['::multistepSubmit'], '#ajax' => [ 'callback' => '::multistepAjax', + 'scope' => 'form', 'wrapper' => 'field-display-overview-wrapper', 'effect' => 'fade', ], @@ -375,7 +408,6 @@ protected function buildFieldRow(FieldDefinitionInterface $field_definition, arr 'settings' => $settings_form, 'third_party_settings' => $third_party_settings_form, 'actions' => [ - '#type' => 'actions', 'save_settings' => $base_button + [ '#type' => 'submit', '#button_type' => 'primary', diff --git a/core/modules/field_ui/src/Form/EntityDisplayModeFormBase.php b/core/modules/field_ui/src/Form/EntityDisplayModeFormBase.php index 72a8a19..ae5f381 100644 --- a/core/modules/field_ui/src/Form/EntityDisplayModeFormBase.php +++ b/core/modules/field_ui/src/Form/EntityDisplayModeFormBase.php @@ -2,6 +2,9 @@ namespace Drupal\field_ui\Form; +use Drupal\Core\Ajax\AjaxResponse; +use Drupal\Core\Ajax\CloseModalDialogCommand; +use Drupal\Core\Ajax\HtmlCommand; use Drupal\Core\Entity\EntityForm; use Drupal\Core\Form\FormStateInterface; @@ -84,4 +87,41 @@ public function save(array $form, FormStateInterface $form_state) { $form_state->setRedirectUrl($this->entity->urlInfo('collection')); } + /** + * {@inheritdoc} + */ + public function actions(array $form, FormStateInterface $form_state) { + $actions = parent::actions($form, $form_state); + $actions['submit']['#ajax'] = [ + 'callback' => '::ajaxCallback', + 'event' => 'click', + ]; + return $actions; + } + + /** + * Ajax callback to close the modal and update the selected text. + * + * @return \Drupal\Core\Ajax\AjaxResponse + * An ajax response object. + */ + public function ajaxCallback(array $form, FormStateInterface $form_state) { + $response = new AjaxResponse(); + if ($form_state->getErrors()) { + unset($form['#prefix'], $form['#suffix']); + $form['status_messages'] = [ + '#type' => 'status_messages', + '#weight' => -10, + ]; + $response->addCommand(new HtmlCommand('#drupal-modal', $form)); + } + else { + // @TODO Show message for successful saving of settings once + // https://www.drupal.org/node/77245 lands. This message is set by + // \Drupal\field_ui\Form\EntityDisplayFormBase::submitForm(). + $response->addCommand(new CloseModalDialogCommand()); + } + return $response; + } + } diff --git a/core/modules/field_ui/src/Routing/RouteSubscriber.php b/core/modules/field_ui/src/Routing/RouteSubscriber.php index b08e7f8..495f14a 100644 --- a/core/modules/field_ui/src/Routing/RouteSubscriber.php +++ b/core/modules/field_ui/src/Routing/RouteSubscriber.php @@ -5,6 +5,7 @@ use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Routing\RouteSubscriberBase; use Drupal\Core\Routing\RoutingEvents; +use Drupal\field_ui\Controller\EntityDisplayModeController; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; @@ -136,7 +137,7 @@ protected function alterRoutes(RouteCollection $collection) { "$path/display", [ '_entity_form' => 'entity_view_display.edit', - '_title' => 'Manage display', + '_title_callback' => EntityDisplayModeController::class . '::editViewDisplayTitle', 'view_mode_name' => 'default', ] + $defaults, ['_field_ui_view_mode_access' => 'administer ' . $entity_type_id . ' display'], @@ -148,7 +149,7 @@ protected function alterRoutes(RouteCollection $collection) { "$path/display/{view_mode_name}", [ '_entity_form' => 'entity_view_display.edit', - '_title' => 'Manage display', + '_title_callback' => EntityDisplayModeController::class . '::editViewDisplayTitle', ] + $defaults, ['_field_ui_view_mode_access' => 'administer ' . $entity_type_id . ' display'], $options diff --git a/core/modules/field_ui/tests/src/Functional/FieldUIRouteTest.php b/core/modules/field_ui/tests/src/Functional/FieldUIRouteTest.php index fe7a270..1b27d27 100644 --- a/core/modules/field_ui/tests/src/Functional/FieldUIRouteTest.php +++ b/core/modules/field_ui/tests/src/Functional/FieldUIRouteTest.php @@ -46,13 +46,13 @@ public function testFieldUIRoutes() { $this->assertResponse(403); $this->drupalGet('admin/config/people/accounts/display'); - $this->assertTitle('Manage display | Drupal'); + $this->assertTitle('Edit Default display of user entities | Drupal'); $this->assertLocalTasks(); $edit = ['display_modes_custom[compact]' => TRUE]; $this->drupalPostForm(NULL, $edit, t('Save')); $this->drupalGet('admin/config/people/accounts/display/compact'); - $this->assertTitle('Manage display | Drupal'); + $this->assertTitle('Edit Compact display of user entities | Drupal'); $this->assertLocalTasks(); // Test manage form display tabs and titles. diff --git a/core/modules/system/tests/src/Functional/Menu/BreadcrumbTest.php b/core/modules/system/tests/src/Functional/Menu/BreadcrumbTest.php index 0e493e5..2e7a4a2 100644 --- a/core/modules/system/tests/src/Functional/Menu/BreadcrumbTest.php +++ b/core/modules/system/tests/src/Functional/Menu/BreadcrumbTest.php @@ -115,10 +115,12 @@ public function testBreadCrumbs() { ]; $this->assertBreadcrumb("admin/structure/types/manage/$type/fields", $trail); $this->assertBreadcrumb("admin/structure/types/manage/$type/display", $trail); + $title_html = t('Edit %view-mode-label display of %bundle-label %entity-type-label', ['%view-mode-label' => 'Teaser', '%bundle-label' => NodeType::load($type)->label(), '%entity-type-label' => 'content items']); $trail_teaser = $trail + [ - "admin/structure/types/manage/$type/display" => t('Manage display'), + // @todo Improve ::assertBreadcrumbParts() + "admin/structure/types/manage/$type/display" => 'Edit display of ', ]; - $this->assertBreadcrumb("admin/structure/types/manage/$type/display/teaser", $trail_teaser); + $this->assertBreadcrumb("admin/structure/types/manage/$type/display/teaser", $trail_teaser, strip_tags($title_html)); $this->assertBreadcrumb("admin/structure/types/manage/$type/delete", $trail); $trail += [ "admin/structure/types/manage/$type/fields" => t('Manage fields'),