From e6a018bcf364663fce5458a949d78d6c12c09136 Mon Sep 17 00:00:00 2001
From: M Parker <mparker17@536298.no-reply.drupal.org>
Date: Wed, 30 Sep 2020 08:27:59 -0400
Subject: [PATCH] 2429699-351 on 9.1.x

---
 .../config/schema/views.filter.schema.yml     |  17 +
 .../Plugin/views/filter/EntityReference.php   | 752 ++++++++++++++++++
 ...iews.view.test_filter_entity_reference.yml | 221 +++++
 core/modules/views/views.views.inc            |   7 +
 .../src/Form/Ajax/ConfigHandlerExtra.php      |   4 +
 .../FilterEntityReferenceWebTest.php          | 219 +++++
 6 files changed, 1220 insertions(+)
 create mode 100644 core/modules/views/src/Plugin/views/filter/EntityReference.php
 create mode 100644 core/modules/views/tests/modules/views_test_config/test_views/views.view.test_filter_entity_reference.yml
 create mode 100644 core/modules/views_ui/tests/src/Functional/FilterEntityReferenceWebTest.php

diff --git a/core/modules/views/config/schema/views.filter.schema.yml b/core/modules/views/config/schema/views.filter.schema.yml
index 1eb09007ae..9ff3c515a7 100644
--- a/core/modules/views/config/schema/views.filter.schema.yml
+++ b/core/modules/views/config/schema/views.filter.schema.yml
@@ -128,6 +128,23 @@ views.filter.many_to_one:
       type: boolean
       label: 'Reduce duplicate'
 
+views.filter.entity_reference:
+  type: views.filter.many_to_one
+  label: 'Entity reference'
+  mapping:
+    handler:
+      type: string
+      label: 'Selection handler'
+    widget:
+      type: string
+      label: 'Selection type'
+    list_max:
+      type: integer
+      label: 'Maximum entities in select list'
+    handler_settings:
+      type: entity_reference_selection.default
+      label: 'Selection handler settings'
+
 views.filter.standard:
   type: views_filter
   label: 'Standard'
diff --git a/core/modules/views/src/Plugin/views/filter/EntityReference.php b/core/modules/views/src/Plugin/views/filter/EntityReference.php
new file mode 100644
index 0000000000..9308ccdb94
--- /dev/null
+++ b/core/modules/views/src/Plugin/views/filter/EntityReference.php
@@ -0,0 +1,752 @@
+<?php
+
+namespace Drupal\views\Plugin\views\filter;
+
+use Drupal\Component\Plugin\DependentPluginInterface;
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Entity\Element\EntityAutocomplete;
+use Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface;
+use Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Form\SubformState;
+use Drupal\Core\Messenger\MessengerInterface;
+use Drupal\Core\Render\Element;
+use Drupal\views\FieldAPIHandlerTrait;
+use Drupal\views\Plugin\views\display\DisplayPluginBase;
+use Drupal\views\ViewExecutable;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Filters a view by entity references.
+ *
+ * @ingroup views_filter_handlers
+ *
+ * @ViewsFilter("entity_reference")
+ */
+class EntityReference extends ManyToOne {
+
+  use FieldAPIHandlerTrait;
+
+  /**
+   * Type for the autocomplete filter format.
+   */
+  const WIDGET_AUTOCOMPLETE = 'autocomplete';
+
+  /**
+   * Type for the select list filter format.
+   */
+  const WIDGET_SELECT = 'select';
+
+  /**
+   * The all value.
+   */
+  const ALL_VALUE = 'All';
+
+  /**
+   * Validated exposed input that will be set as value in case.
+   *
+   * @var array
+   */
+  protected $validatedExposedInput;
+
+  /**
+   * The selection plugin manager service.
+   *
+   * @var \Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface
+   */
+  protected $selectionPluginManager;
+
+  /**
+   * The entity type manager service.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * The messenger service for setting messages.
+   *
+   * @var \Drupal\Core\Messenger\MessengerInterface
+   */
+  protected $messenger;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(
+    array $configuration,
+    $plugin_id,
+    $plugin_definition,
+    SelectionPluginManagerInterface $selection_plugin_manager,
+    EntityTypeManagerInterface $entity_type_manager,
+    MessengerInterface $messenger
+  ) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $this->selectionPluginManager = $selection_plugin_manager;
+    $this->entityTypeManager = $entity_type_manager;
+    $this->messenger = $messenger;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): EntityReference {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('plugin.manager.entity_reference_selection'),
+      $container->get('entity_type.manager'),
+      $container->get('messenger')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
+    parent::init($view, $display, $options);
+
+    if (empty($this->definition['field_name'])) {
+      $this->definition['field_name'] = $options['field'];
+    }
+
+    $this->definition['options callback'] = [$this, 'getValueOptionsCallback'];
+    $this->definition['options arguments'] = [$this->getSelectionHandler()];
+  }
+
+  /**
+   * Get the used entity reference selection handler.
+   *
+   * @return \Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface
+   *   The selection handler plugin instance.
+   */
+  protected function getSelectionHandler(): SelectionInterface {
+    $handler_settings = $this->options['handler_settings'] + [
+      'target_type' => $this->getReferencedEntityType()->id(),
+      'handler' => $this->options['handler'],
+    ];
+    return $this->selectionPluginManager->getInstance($handler_settings);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function defineOptions(): array {
+    $options = parent::defineOptions();
+
+    $options['handler'] = ['default' => 'default:' . $this->getReferencedEntityType()->id()];
+    $options['handler_settings'] = ['default' => []];
+    $options['widget'] = ['default' => self::WIDGET_AUTOCOMPLETE];
+    $options['list_max'] = ['default' => '100'];
+
+    return $options;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function hasExtraOptions(): bool {
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildExtraOptionsForm(&$form, FormStateInterface $form_state) {
+    $entity_type = $this->getReferencedEntityType();
+
+    // Get all selection plugins for this entity type.
+    $selection_plugins = $this->selectionPluginManager->getSelectionGroups($entity_type->id());
+    $handlers_options = [];
+    foreach (array_keys($selection_plugins) as $selection_group_id) {
+      // We only display base plugins (e.g. 'default', 'views', ...) and not
+      // entity type specific plugins (e.g. 'default:node', 'default:user',
+      // ...).
+      if (array_key_exists($selection_group_id, $selection_plugins[$selection_group_id])) {
+        $handlers_options[$selection_group_id] = $selection_plugins[$selection_group_id][$selection_group_id]['label'];
+      }
+      elseif (array_key_exists($selection_group_id . ':' . $entity_type->id(), $selection_plugins[$selection_group_id])) {
+        $selection_group_plugin = $selection_group_id . ':' . $entity_type->id();
+        $handlers_options[$selection_group_plugin] = $selection_plugins[$selection_group_id][$selection_group_plugin]['base_plugin_label'];
+      }
+    }
+
+    // @todo When changing selection handler Ajax request doesn't get processed
+    // correctly so need to add this hack.
+    $input = $form_state->getUserInput();
+    if (isset($input['options']['handler']) && array_key_exists($input['options']['handler'], $handlers_options)) {
+      $this->options['handler'] = $input['options']['handler'];
+    }
+    if (isset($input['options']['handler_settings'])) {
+      $this->options['handler_settings'] = $input['options']['handler_settings'];
+    }
+
+    $form['#process'] = [[get_class($this), 'extraOptionsAjaxProcess']];
+
+    // @todo: We would actually prefer organizing the form elements according
+    // to the required structure of the value tree, and to rearrange the visual
+    // grouping using the #group key, in order to avoid messing with #parents.
+    // Currently, this however isn't possible. Revisit once Core issue
+    // https://www.drupal.org/project/drupal/issues/2854166 landed.
+    $form['reference'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Reference type'),
+      '#open' => TRUE,
+      '#parents' => ['options'],
+    ];
+
+    $form['reference']['handler'] = [
+      '#type' => 'select',
+      '#title' => $this->t('Reference method'),
+      '#options' => $handlers_options,
+      '#default_value' => $this->options['handler'],
+      '#required' => TRUE,
+      '#ajax' => TRUE,
+      '#limit_validation_errors' => [['options', 'handler']],
+    ];
+
+    $form['reference']['handler_submit'] = [
+      '#type' => 'submit',
+      '#value' => $this->t('Change handler'),
+      '#limit_validation_errors' => [],
+      '#attributes' => [
+        'class' => ['js-hide'],
+      ],
+      '#submit' => [[get_class($this), 'settingsAjaxSubmit']],
+    ];
+
+    $form['reference']['handler_settings'] = [
+      '#type' => 'container',
+      '#attributes' => ['class' => ['entity_reference-settings']],
+      '#process' => [[get_class($this), 'fixSubmitParents']],
+    ];
+
+    $selection_handler = $this->getSelectionHandler();
+    $subform_state = SubformState::createForSubform($form['reference'], $form, $form_state);
+    $form['reference']['handler_settings'] += $selection_handler->buildConfigurationForm([], $subform_state);
+
+    // Remove ajax to load handler target bundles immediately.
+    $form['reference']['handler_settings']['target_bundles']['#ajax'] = FALSE;
+
+    // Remove unnecessary handler settings from the filter config form.
+    $form['reference']['handler_settings']['target_bundles_update']['#access'] = FALSE;
+    $form['reference']['handler_settings']['auto_create']['#access'] = FALSE;
+    $form['reference']['handler_settings']['auto_create_bundle']['#access'] = FALSE;
+
+    $form['widget'] = [
+      '#type' => 'radios',
+      '#title' => $this->t('Selection type'),
+      '#default_value' => $this->options['widget'],
+      '#options' => [
+        self::WIDGET_SELECT => $this->t('Select list'),
+        self::WIDGET_AUTOCOMPLETE => $this->t('Autocomplete'),
+      ],
+    ];
+
+    $form['list_max'] = [
+      '#type' => 'number',
+      '#title' => $this->t('Maximum entities in select list'),
+      '#description' => $this->t('This is the limit to the number of entities to allow for select type of widget. It is strongly recommended to set a limit to avoid performance issues. When this limit is reached the widget switches automatically to an autocomplete widget. Set the limit to "0" to allow unlimited entities.'),
+      '#default_value' => $this->options['list_max'],
+      '#min' => 0,
+      '#max' => 1000,
+      '#states' => [
+        'visible' => [
+          ':input[name="options[widget]"]' => ['value' => static::WIDGET_SELECT],
+        ],
+      ],
+    ];
+  }
+
+  /**
+   * Render API callback.
+   *
+   * Processes the field settings form and allows access to the form state.
+   *
+   * @param array $form
+   *   Associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   *
+   * @return array
+   *   Associative array containing the structure of the form.
+   *
+   * @see static::buildExtraOptionsForm()
+   */
+  public static function fixSubmitParents(array $form, FormStateInterface $form_state): array {
+    static::fixSubmitParentsElement($form, 'root');
+    return $form;
+  }
+
+  /**
+   * Process element callback.
+   *
+   * Adds entity_reference specific properties to AJAX form elements from the
+   * field settings form.
+   *
+   * @param array $element
+   *   Associative array containing the structure of the form, subform or form
+   *   element, passed by reference.
+   * @param string $key
+   *   The element key, or 'root'.
+   *
+   * @see static::fixSubmitParents()
+   */
+  public static function fixSubmitParentsElement(array &$element, $key) {
+    if (isset($element['#type']) && in_array($element['#type'], ['button', 'submit']) && $key !== 'root') {
+      $element['#parents'] = [$key];
+    }
+
+    foreach (Element::children($element) as $key) {
+      static::fixSubmitParentsElement($element[$key], $key);
+    }
+  }
+
+  /**
+   * Render API callback.
+   *
+   * Processes the extra options form and allows access to the form state.
+   *
+   * @see static::buildExtraOptionsForm()
+   * @see \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem::fieldSettingsAjaxProcess()
+   */
+  public static function extraOptionsAjaxProcess(array $form, FormStateInterface $form_state): array {
+    static::extraOptionsAjaxProcessElement($form, $form, $form_state);
+    return $form;
+  }
+
+  /**
+   * Process element callback.
+   *
+   * Adds entity_reference specific properties to AJAX form elements from the
+   * extra options form.
+   *
+   * @param array $element
+   *   Associative array containing the structure of the form, subform or form
+   *   element to be processed, passed by reference.
+   * @param array $main_form
+   *   Associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   *
+   * @see static::extraOptionsAjaxProcess()
+   * @see \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem::fieldSettingsAjaxProcessElement()
+   */
+  public static function extraOptionsAjaxProcessElement(array &$element, array $main_form, FormStateInterface $form_state) {
+    if (!empty($element['#ajax'])) {
+      $element['#ajax'] = [
+        'callback' => [get_called_class(), 'settingsAjax'],
+        'url' => views_ui_build_form_url($form_state),
+        'wrapper' => $main_form['#id'],
+        'element' => $main_form['#array_parents'],
+      ];
+    }
+
+    foreach (Element::children($element) as $key) {
+      static::extraOptionsAjaxProcessElement($element[$key], $main_form, $form_state);
+    }
+  }
+
+  /**
+   * Ajax callback for the handler settings form.
+   *
+   * @param array $form
+   *   Associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   *
+   * @return array
+   *   Settings form array for the triggering element.
+   *
+   * @see static::extraOptionsAjaxProcessElement()
+   * @see \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem::settingsAjax()
+   */
+  public static function settingsAjax(array $form, FormStateInterface $form_state): array {
+    $triggering_element = $form_state->getTriggeringElement();
+    return NestedArray::getValue($form, $triggering_element['#ajax']['element']);
+  }
+
+  /**
+   * Submit handler for the non-JS case.
+   *
+   * @param array $form
+   *   Associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   *
+   * @see \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem::settingsAjaxSubmit()
+   */
+  public static function settingsAjaxSubmit(array $form, FormStateInterface $form_state) {
+    $form_state->set('rerender', TRUE);
+    $form_state->setRebuild();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateExtraOptionsForm($form, FormStateInterface $form_state) {
+    $subform_state = SubformState::createForSubform($form['reference'], $form, $form_state);
+
+    // Copy handler_settings from options to settings to be compatible with
+    // selection plugins.
+    $subform_state->setValue(['settings', 'handler_settings'], $form_state->getValue(['options', 'handler_settings']));
+
+    $this->getSelectionHandler()->validateConfigurationForm($form, $subform_state);
+
+    // Copy handler_settings back into options.
+    // Necessary because DefaultSelection::validateConfigurationForm()
+    // manipulates the form state values.
+    $form_state->setValue(['options', 'handler_settings'], $subform_state->getValue(['settings', 'handler_settings']));
+
+    parent::validateExtraOptionsForm($form, $form_state);
+  }
+
+  /**
+   * Fixes the issue with switching between the widgets in the view editor.
+   *
+   * @param array $form
+   *   Associative array containing the structure of the form, passed by
+   *   reference.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   */
+  protected function alternateWidgetsDefaultNormalize(array &$form, FormStateInterface $form_state) {
+    $field_id = '_' . $this->getFieldDefinition()->getName() . '-widget';
+    $form[$field_id] = [
+      '#type' => 'hidden',
+      '#value' => $this->options['widget'],
+    ];
+
+    $previous_widget = $form_state->getUserInput()[$field_id] ?? NULL;
+    if ($previous_widget && $previous_widget !== $this->options['widget']) {
+      $form['value']['#value_callback'] = function ($element) {
+        return $element['#default_value'] ?? '';
+      };
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function valueForm(&$form, FormStateInterface $form_state) {
+    switch ($this->options['widget']) {
+      case self::WIDGET_SELECT:
+        $list_max = $this->options['list_max'] > 0 ? $this->options['list_max'] : 0;
+        $options = $this->getValueOptions();
+        if ($list_max <= 0 || count($options) <= $list_max) {
+          $this->valueFormAddSelect($form, $form_state);
+        }
+        else {
+          $this->options['widget'] = self::WIDGET_AUTOCOMPLETE;
+          $this->valueFormAddAutocomplete($form, $form_state);
+
+          if (!empty($this->view->live_preview)) {
+            $this->messenger->addWarning($this->t("Limit of maximum :limit entities reached for entity reference filter ':filter', switching to autocomplete widget.", [
+              ':filter' => $this->getFieldDefinition()->getName(),
+              ':limit' => $list_max,
+            ]));
+          }
+        }
+        break;
+
+      case self::WIDGET_AUTOCOMPLETE:
+        $this->valueFormAddAutocomplete($form, $form_state);
+        break;
+    }
+
+    if (!empty($this->view->live_preview)) {
+      $this->alternateWidgetsDefaultNormalize($form, $form_state);
+    }
+
+    // Show or hide the value field depending on the operator field.
+    $is_exposed = $form_state->get('exposed');
+
+    $visible = [];
+    if ($is_exposed) {
+      $operator_field = ($this->options['expose']['use_operator'] && $this->options['expose']['operator_id']) ? $this->options['expose']['operator_id'] : NULL;
+    }
+    else {
+      $operator_field = 'options[operator]';
+      $visible[] = [
+        ':input[name="options[expose_button][checkbox][checkbox]"]' => ['checked' => TRUE],
+        ':input[name="options[expose][use_operator]"]' => ['checked' => TRUE],
+        ':input[name="options[expose][operator_id]"]' => ['empty' => FALSE],
+      ];
+    }
+    if ($operator_field) {
+      foreach ($this->operatorValues(1) as $operator) {
+        $visible[] = [
+          ':input[name="' . $operator_field . '"]' => ['value' => $operator],
+        ];
+      }
+      $form['value']['#states'] = ['visible' => $visible];
+    }
+
+    if (!$is_exposed) {
+      // Retain the helper option.
+      $this->helper->buildOptionsForm($form, $form_state);
+
+      // Show help text if not exposed to end users.
+      $form['value']['#description'] = $this->t('Leave blank for all. Otherwise, the first selected item will be the default instead of "Any".');
+    }
+  }
+
+  /**
+   * ValueForm helper adding an autocomplete element to the form.
+   *
+   * @param array $form
+   *   Associative array containing the structure of the form, passed by
+   *   reference.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   */
+  protected function valueFormAddAutocomplete(array &$form, FormStateInterface $form_state) {
+    $referenced_type = $this->getReferencedEntityType();
+    $form['value'] = [
+      '#title' => $this->t('Select %entity_types', ['%entity_types' => $referenced_type->getPluralLabel()]),
+      '#type' => 'entity_autocomplete',
+      '#default_value' => EntityAutocomplete::getEntityLabels($this->getDefaultSelectedEntities()),
+      '#tags' => TRUE,
+      '#process_default_value' => FALSE,
+      '#target_type' => $this->getReferencedEntityType()->id(),
+      '#selection_handler' => $this->options['handler'],
+      '#selection_settings' => $this->options['handler_settings'],
+      '#validate_reference' => FALSE,
+    ];
+  }
+
+  /**
+   * ValueForm helper adding a select element to the form.
+   *
+   * @param array $form
+   *   Associative array containing the structure of the form, passed by
+   *   reference.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   */
+  protected function valueFormAddSelect(array &$form, FormStateInterface $form_state) {
+    $is_exposed = $form_state->get('exposed');
+
+    $options = $this->getValueOptions();
+    $default_value = (array) $this->value;
+
+    if ($is_exposed) {
+      $identifier = $this->options['expose']['identifier'];
+
+      if (!empty($this->options['expose']['reduce'])) {
+        $options = $this->reduceValueOptions($options);
+
+        if (!empty($this->options['expose']['multiple']) && empty($this->options['expose']['required'])) {
+          $default_value = [];
+        }
+      }
+
+      if (empty($this->options['expose']['multiple'])) {
+        if (empty($this->options['expose']['required']) && (empty($default_value) || !empty($this->options['expose']['reduce']))) {
+          $default_value = self::ALL_VALUE;
+        }
+        elseif (empty($default_value)) {
+          $keys = array_keys($options);
+          $default_value = array_shift($keys);
+        }
+        // Due to https://www.drupal.org/node/1464174 there is a chance that
+        // [''] was saved in the admin ui. Let's choose a safe default value.
+        elseif ($default_value == ['']) {
+          $default_value = self::ALL_VALUE;
+        }
+        else {
+          $copy = $default_value;
+          $default_value = array_shift($copy);
+        }
+      }
+    }
+
+    $referenced_type = $this->getReferencedEntityType();
+    $form['value'] = [
+      '#type' => 'select',
+      '#title' => $this->t('Select @entity_types', ['@entity_types' => $referenced_type->getPluralLabel()]),
+      '#multiple' => TRUE,
+      '#options' => $options,
+      '#size' => min(9, count($options)),
+      '#default_value' => $default_value,
+    ];
+
+    $user_input = $form_state->getUserInput();
+    if ($is_exposed && isset($identifier) && !isset($user_input[$identifier])) {
+      $user_input[$identifier] = $default_value;
+      $form_state->setUserInput($user_input);
+    }
+  }
+
+  /**
+   * Gets all entities selected by default.
+   *
+   * @return \Drupal\Core\Entity\EntityInterface[]
+   *   All entities selected by default, or an empty array, if none.
+   */
+  protected function getDefaultSelectedEntities(): array {
+    $referenced_type_id = $this->getReferencedEntityType()->id();
+    $entity_storage = $this->entityTypeManager->getStorage($referenced_type_id);
+
+    return $this->value && !isset($this->value[self::ALL_VALUE]) ? $entity_storage->loadMultiple($this->value) : [];
+  }
+
+  /**
+   * Value options callback.
+   *
+   * @param \Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface $selection_handler
+   *   The selection handler.
+   *
+   * @return string[]
+   *   The options.
+   *
+   * @see \Drupal\views\Plugin\views\filter\InOperator::getValueOptions()
+   */
+  protected function getValueOptionsCallback(SelectionInterface $selection_handler): array {
+    // Get the "Maximum entities in select list" value as an integer and set it
+    // to 0 if no maximum is set.
+    $list_max = !empty($this->options['list_max']) ? (int) $this->options['list_max'] : 0;
+
+    // If there is a maximum set, get one more than the maximum
+    // referencable entities to determine if the widget should be switched to
+    // autocomplete.
+    $limit = ($list_max > 0 ? ($list_max + 1) : $list_max);
+    $entities = $selection_handler->getReferenceableEntities(NULL, 'CONTAINS', $limit);
+
+    $options = [];
+    foreach ($entities as $bundle) {
+      foreach ($bundle as $id => $entity_label) {
+        $options[$id] = $entity_label;
+      }
+    }
+
+    return $options;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function valueValidate($form, FormStateInterface $form_state) {
+    if ($this->options['widget'] == self::WIDGET_AUTOCOMPLETE) {
+      $ids = [];
+      if ($values = $form_state->getValue(['options', 'value'])) {
+        foreach ($values as $value) {
+          $ids[] = $value['target_id'];
+        }
+      }
+      $form_state->setValue(['options', 'value'], $ids);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function acceptExposedInput($input): bool {
+    if (empty($this->options['exposed'])) {
+      return TRUE;
+    }
+    // We need to know the operator, which is normally set in
+    // \Drupal\views\Plugin\views\filter\FilterPluginBase::acceptExposedInput(),
+    // before we actually call the parent version of ourselves.
+    if (!empty($this->options['expose']['use_operator']) && !empty($this->options['expose']['operator_id']) && isset($input[$this->options['expose']['operator_id']])) {
+      $this->operator = $input[$this->options['expose']['operator_id']];
+    }
+
+    // If view is an attachment and is inheriting exposed filters, then assume
+    // exposed input has already been validated.
+    if (!empty($this->view->is_attachment) && $this->view->display_handler->usesExposed()) {
+      $this->validatedExposedInput = (array) $this->view->exposed_raw_input[$this->options['expose']['identifier']];
+    }
+
+    // If we're checking for EMPTY or NOT, we don't need any input, and we can
+    // say that our input conditions are met by just having the right operator.
+    if ($this->operator == 'empty' || $this->operator == 'not empty') {
+      return TRUE;
+    }
+
+    // If it's non-required and there's no value don't bother filtering.
+    if (!$this->options['expose']['required'] && empty($this->validatedExposedInput)) {
+      return FALSE;
+    }
+
+    $rc = parent::acceptExposedInput($input);
+    if ($rc) {
+      // If we have previously validated input, override.
+      if (isset($this->validatedExposedInput)) {
+        $this->value = $this->validatedExposedInput;
+      }
+    }
+
+    return $rc;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateExposed(&$form, FormStateInterface $form_state) {
+    if (empty($this->options['exposed'])) {
+      return;
+    }
+
+    $identifier = $this->options['expose']['identifier'];
+
+    // We only validate if they've chosen the select field style.
+    if ($this->options['widget'] == self::WIDGET_SELECT) {
+      if ($form_state->getValue($identifier) != self::ALL_VALUE) {
+        $this->validatedExposedInput = (array) $form_state->getValue($identifier);
+      }
+      return;
+    }
+
+    if (empty($identifier)) {
+      return;
+    }
+
+    if ($values = $form_state->getValue($identifier)) {
+      foreach ($values as $value) {
+        $this->validatedExposedInput[] = $value['target_id'];
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function valueSubmit($form, FormStateInterface $form_state) {
+    // Prevent array_filter from messing up our arrays in parent submit.
+  }
+
+  /**
+   * Gets the target entity type ID referenced by this field.
+   *
+   * @return \Drupal\Core\Entity\EntityTypeInterface
+   *   Entity type.
+   */
+  protected function getReferencedEntityType(): EntityTypeInterface {
+    $field_def = $this->getFieldDefinition();
+    $entity_type_id = $field_def->getItemDefinition()->getSetting('target_type');
+    return $this->entityTypeManager->getDefinition($entity_type_id);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function calculateDependencies(): array {
+    $dependencies = parent::calculateDependencies();
+
+    $selection_handler = $this->getSelectionHandler();
+    if ($selection_handler instanceof DependentPluginInterface) {
+      $dependencies += $selection_handler->calculateDependencies();
+    }
+
+    foreach ($this->getDefaultSelectedEntities() as $entity) {
+      $dependencies[$entity->getConfigDependencyKey()][] = $entity->getConfigDependencyName();
+    }
+
+    return $dependencies;
+  }
+
+}
diff --git a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_filter_entity_reference.yml b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_filter_entity_reference.yml
new file mode 100644
index 0000000000..2565e43083
--- /dev/null
+++ b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_filter_entity_reference.yml
@@ -0,0 +1,221 @@
+langcode: en
+status: true
+dependencies:
+  config:
+      - node.type.page
+  module:
+      - node
+      - user
+id: test_filter_entity_reference
+label: test_filter_entity_reference
+module: views
+description: ''
+tag: ''
+base_table: node_field_data
+base_field: nid
+display:
+  default:
+    display_plugin: default
+    id: default
+    display_title: Master
+    position: 0
+    display_options:
+      access:
+        type: perm
+        options:
+          perm: 'access content'
+      cache:
+        type: none
+        options: {  }
+      query:
+        type: views_query
+        options:
+          disable_sql_rewrite: false
+          distinct: false
+          replica: false
+          query_comment: ''
+          query_tags: {  }
+      exposed_form:
+        type: basic
+        options:
+          submit_button: Apply
+          reset_button: false
+          reset_button_label: Reset
+          exposed_sorts_label: 'Sort by'
+          expose_sort_order: true
+          sort_asc_label: Asc
+          sort_desc_label: Desc
+      pager:
+        type: mini
+        options:
+          items_per_page: 10
+          offset: 0
+          id: 0
+          total_pages: null
+          expose:
+            items_per_page: false
+            items_per_page_label: 'Items per page'
+            items_per_page_options: '5, 10, 25, 50'
+            items_per_page_options_all: false
+            items_per_page_options_all_label: '- All -'
+            offset: false
+            offset_label: Offset
+          tags:
+            previous: Ã¢â‚¬Â¹Ã¢â‚¬Â¹
+            next: Ã¢â‚¬ÂºÃ¢â‚¬Âº
+      style:
+        type: default
+        options:
+          grouping: {  }
+          row_class: ''
+          default_row_class: true
+          uses_fields: false
+      row:
+        type: fields
+        options:
+          inline: {  }
+          separator: ''
+          hide_empty: false
+          default_field_elements: true
+      fields:
+        title:
+          id: title
+          table: node_field_data
+          field: title
+          entity_type: node
+          entity_field: title
+          label: ''
+          alter:
+            alter_text: false
+            make_link: false
+            absolute: false
+            trim: false
+            word_boundary: false
+            ellipsis: false
+            strip_tags: false
+            html: false
+          hide_empty: false
+          empty_zero: false
+          settings:
+            link_to_entity: true
+          plugin_id: field
+          relationship: none
+          group_type: group
+          admin_label: ''
+          exclude: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_alter_empty: true
+          click_sort_column: value
+          type: string
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+      filters:
+        status:
+          value: '1'
+          table: node_field_data
+          field: status
+          plugin_id: boolean
+          entity_type: node
+          entity_field: status
+          id: status
+          expose:
+            operator: ''
+          group: 1
+        type:
+          id: type
+          table: node_field_data
+          field: type
+          value:
+            page: page
+          entity_type: node
+          entity_field: type
+          plugin_id: bundle
+        field_test_target_id:
+          id: field_test_target_id
+          table: node__field_test
+          field: field_test_target_id
+          relationship: none
+          group_type: group
+          admin_label: ''
+          operator: or
+          value: {  }
+          group: 1
+          exposed: true
+          expose:
+            operator_id: field_test_target_id_op
+            label: 'Test (field_test)'
+            description: ''
+            use_operator: false
+            operator: field_test_target_id_op
+            identifier: field_test_target_id
+            required: false
+            remember: false
+            multiple: true
+            remember_roles:
+              authenticated: authenticated
+              anonymous: '0'
+              administrator: '0'
+            reduce: false
+          is_grouped: false
+          group_info:
+            label: ''
+            description: ''
+            identifier: ''
+            optional: true
+            widget: select
+            multiple: false
+            remember: false
+            default_group: All
+            default_group_multiple: {  }
+            group_items: {  }
+          reduce_duplicates: false
+          handler: 'default:node'
+          handler_settings:
+            target_bundles:
+              article: article
+            sort:
+              field: title
+              direction: ASC
+            auto_create: false
+            auto_create_bundle: ''
+          widget: select
+          list_max: 100
+          plugin_id: entity_reference
+      sorts:
+        created:
+          id: created
+          table: node_field_data
+          field: created
+          order: DESC
+          entity_type: node
+          entity_field: created
+          plugin_id: date
+          relationship: none
+          group_type: group
+          admin_label: ''
+          exposed: false
+          expose:
+            label: ''
+          granularity: second
+      header: {  }
+      footer: {  }
+      empty: {  }
+      relationships: {  }
+      arguments: {  }
+      display_extenders: {  }
diff --git a/core/modules/views/views.views.inc b/core/modules/views/views.views.inc
index 88c342ef7e..6888ba6a6f 100644
--- a/core/modules/views/views.views.inc
+++ b/core/modules/views/views.views.inc
@@ -811,6 +811,13 @@ function core_field_views_data(FieldStorageConfigInterface $field_storage) {
         'relationship field' => $field_name . '_target_id',
       ];
 
+      // Render filters as select lists.
+      foreach ($table_data as $table_field_name => $table_field_data) {
+        if (isset($table_field_data['filter']) && $table_field_name != 'delta') {
+          $data[$table_name][$table_field_name]['filter']['id'] = 'entity_reference';
+        }
+      }
+
       // Provide a reverse relationship for the entity type that is referenced by
       // the field.
       $args['@entity'] = $entity_type->getLabel();
diff --git a/core/modules/views_ui/src/Form/Ajax/ConfigHandlerExtra.php b/core/modules/views_ui/src/Form/Ajax/ConfigHandlerExtra.php
index 8e71123389..ff7f53ce6b 100644
--- a/core/modules/views_ui/src/Form/Ajax/ConfigHandlerExtra.php
+++ b/core/modules/views_ui/src/Form/Ajax/ConfigHandlerExtra.php
@@ -95,6 +95,10 @@ public function buildForm(array $form, FormStateInterface $form_state) {
    */
   public function validateForm(array &$form, FormStateInterface $form_state) {
     $form_state->get('handler')->validateExtraOptionsForm($form['options'], $form_state);
+
+    if ($form_state->getErrors()) {
+      $form_state->set('rerender', TRUE);
+    }
   }
 
   /**
diff --git a/core/modules/views_ui/tests/src/Functional/FilterEntityReferenceWebTest.php b/core/modules/views_ui/tests/src/Functional/FilterEntityReferenceWebTest.php
new file mode 100644
index 0000000000..de704a260a
--- /dev/null
+++ b/core/modules/views_ui/tests/src/Functional/FilterEntityReferenceWebTest.php
@@ -0,0 +1,219 @@
+<?php
+
+namespace Drupal\Tests\views_ui\Functional;
+
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\Component\Render\FormattableMarkup;
+use Drupal\node\NodeInterface;
+
+/**
+ * Test the entity reference filter UI.
+ *
+ * @group views_ui
+ * @see \Drupal\views\Plugin\views\filter\EntityReference
+ */
+class FilterEntityReferenceWebTest extends UITestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stark';
+
+  /**
+   * Entity type and referenceable type.
+   *
+   * @var \Drupal\node\NodeTypeInterface
+   */
+  protected $entityType;
+
+  /**
+   * Referenceable entity type.
+   *
+   * @var \Drupal\node\NodeTypeInterface
+   */
+  protected $referenceableType;
+
+  /**
+   * Referenceable content.
+   *
+   * @var \Drupal\node\NodeInterface[]
+   */
+  protected $nodes;
+
+  /**
+   * Content containing fields as reference.
+   *
+   * @var \Drupal\node\NodeInterface[]
+   */
+  protected $containingNodes;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $testViews = ['test_filter_entity_reference'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp($import_test_views = TRUE): void {
+    parent::setUp($import_test_views);
+
+    // Create an entity type, and a referenceable type. Since these are coded
+    // into the test view, they are not randomly named.
+    $this->entityType = $this->drupalCreateContentType(['type' => 'page']);
+    $this->referenceableType = $this->drupalCreateContentType(['type' => 'article']);
+
+    $field_storage = FieldStorageConfig::create([
+      'entity_type' => 'node',
+      'field_name' => 'field_test',
+      'type' => 'entity_reference',
+      'settings' => [
+        'target_type' => 'node',
+      ],
+      'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
+    ]);
+    $field_storage->save();
+
+    $field = FieldConfig::create([
+      'entity_type' => 'node',
+      'field_name' => 'field_test',
+      'bundle' => $this->entityType->id(),
+      'settings' => [
+        'handler' => 'default',
+        'handler_settings' => [
+          // Note, this has no impact on Views at this time.
+          'target_bundles' => [
+            $this->referenceableType->id() => $this->referenceableType->label(),
+          ],
+        ],
+      ],
+    ]);
+    $field->save();
+
+    // Create 10 referenceable nodes.
+    for ($i = 0; $i < 10; $i++) {
+      $node = $this->drupalCreateNode(['type' => $this->referenceableType->id()]);
+      $this->nodes[$node->id()] = $node;
+    }
+
+    $node = $this->drupalCreateNode(['type' => $this->entityType->id()]);
+    $this->containingNodes = [
+      $node->id() => $node,
+    ];
+  }
+
+  /**
+   * Tests the filter UI.
+   */
+  public function testFilterUi() {
+    $this->drupalGet('admin/structure/views/nojs/handler/test_filter_entity_reference/default/filter/field_test_target_id');
+
+    $options = $this->getUiOptions();
+    // Should be sorted by title ASC.
+    uasort($this->nodes, function (NodeInterface $a, NodeInterface $b) {
+      return strnatcasecmp($a->getTitle(), $b->getTitle());
+    });
+    $found_all = TRUE;
+    $i = 0;
+    foreach ($this->nodes as $nid => $node) {
+      $option = $options[$i];
+      $label = $option['label'];
+      $found_all = $found_all && $label == $node->label() && $nid == $option['nid'];
+      $this->assertEqual($label, $node->label(), new FormattableMarkup('Expected referenceable label found for option :option', [':option' => $i]));
+      $i++;
+    }
+    $this->assertTrue($found_all, 'All referenceable nodes were available as a select list properly ordered.');
+
+    // Change the sort field and direction.
+    $edit = [
+      'options[handler_settings][sort][field]' => 'nid',
+      'options[handler_settings][sort][direction]' => 'DESC',
+    ];
+    $this->drupalPostForm('admin/structure/views/nojs/handler-extra/test_filter_entity_reference/default/filter/field_test_target_id', $edit, t('Apply'));
+
+    $this->drupalGet('admin/structure/views/nojs/handler/test_filter_entity_reference/default/filter/field_test_target_id');
+    // Items should now be in reverse nid order.
+    krsort($this->nodes);
+    $options = $this->getUiOptions();
+    $found_all = TRUE;
+    $i = 0;
+    foreach ($this->nodes as $nid => $node) {
+      $option = $options[$i];
+      $label = $option['label'];
+      $found_all = $found_all && $label == $node->label() && $nid == $option['nid'];
+      $this->assertEqual($label, $node->label(), new FormattableMarkup('Expected referenceable label found for option :option', [':option' => $i]));
+      $i++;
+    }
+    $this->assertTrue($found_all, 'All referenceable nodes were available as a select list properly ordered.');
+
+    // Change bundle types.
+    $edit = [
+      "options[handler_settings][target_bundles][{$this->entityType->id()}]" => TRUE,
+      "options[handler_settings][target_bundles][{$this->referenceableType->id()}]" => TRUE,
+    ];
+    $this->drupalPostForm('admin/structure/views/nojs/handler-extra/test_filter_entity_reference/default/filter/field_test_target_id', $edit, t('Apply'));
+
+    $this->drupalGet('admin/structure/views/nojs/handler/test_filter_entity_reference/default/filter/field_test_target_id');
+    $options = $this->getUiOptions();
+    $found_all = TRUE;
+    $i = 0;
+    foreach ($this->containingNodes + $this->nodes as $nid => $node) {
+      $option = $options[$i];
+      $label = $option['label'];
+      $found_all = $found_all && $label == $node->label() && $nid == $option['nid'];
+      $this->assertEqual($label, $node->label(), new FormattableMarkup('Expected referenceable label found for option :option', [':option' => $i]));
+      $i++;
+    }
+    $this->assertTrue($found_all, 'All referenceable nodes were available from both bundles.');
+
+    // Reduce maximum select items, so the widget automatically switches to an
+    // autocomplete.
+    $edit = [
+      'options[list_max]' => 2,
+      'options[handler_settings][target_bundles][page]' => FALSE,
+    ];
+    $this->drupalPostForm('admin/structure/views/nojs/handler-extra/test_filter_entity_reference/default/filter/field_test_target_id', $edit, t('Apply'));
+
+    $this->drupalGet('admin/structure/views/nojs/handler/test_filter_entity_reference/default/filter/field_test_target_id');
+    $autocompletes = $this->xpath('//input[@name="options[value]"]');
+    $this->assertNotEmpty($autocompletes, 'Autocomplete filter field found when select threshold exceeded.');
+    $this->assertNotEmpty($autocompletes[0]->getAttribute('data-autocomplete-path'), 'Autocomplete path was set on filter field');
+
+    // Now explicitly change widget mode.
+    $edit = [
+      'options[widget]' => 'autocomplete',
+    ];
+    $this->drupalPostForm('admin/structure/views/nojs/handler-extra/test_filter_entity_reference/default/filter/field_test_target_id', $edit, t('Apply'));
+
+    $this->drupalGet('admin/structure/views/nojs/handler/test_filter_entity_reference/default/filter/field_test_target_id');
+    $autocompletes = $this->xpath('//input[@name="options[value]"]');
+    $this->assertNotEmpty($autocompletes, 'Autocomplete filter field found when widget set to autocomplete');
+    $this->assertNotEmpty($autocompletes[0]->getAttribute('data-autocomplete-path'), 'Autocomplete path was set on filter field');
+  }
+
+  /**
+   * Helper method to parse options from the UI.
+   *
+   * @return array
+   *   Array of keyed arrays containing `nid` and `label` of each option.
+   */
+  protected function getUiOptions() {
+    /** @var \Behat\Mink\Element\NodeElement[] $result */
+    $result = $this->xpath('//select[@name="options[value][]"]/option');
+    $this->assertNotEmpty($result, 'Options found');
+
+    $options = [];
+    foreach ($result as $option) {
+      $nid = (int) $option->getValue();
+      $options[] = [
+        'nid' => $nid,
+        'label' => (string) $this->getSession()->getDriver()->getText($option->getXpath()),
+      ];
+    }
+
+    return $options;
+  }
+
+}
-- 
2.25.1

