Files
farmOS-dev/web/profiles/farm/patches/3347343-10-3-x--comment-214-onwards--do-not-merge-2024-09-01.patch
2024-12-10 15:08:16 +01:00

2218 lines
74 KiB
Diff

diff --git a/core/lib/Drupal/Core/Entity/Plugin/EntityReferenceSelection/DefaultSelection.php b/core/lib/Drupal/Core/Entity/Plugin/EntityReferenceSelection/DefaultSelection.php
index 17dcac0a90e9ba1132cf4a4ab685fed40bfe1a5a..afe418450ae4a321f77d67b6dac161f2f71813a1 100644
--- a/core/lib/Drupal/Core/Entity/Plugin/EntityReferenceSelection/DefaultSelection.php
+++ b/core/lib/Drupal/Core/Entity/Plugin/EntityReferenceSelection/DefaultSelection.php
@@ -164,6 +164,7 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
$entity_type_id = $configuration['target_type'];
$entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
$bundles = $this->entityTypeBundleInfo->getBundleInfo($entity_type_id);
+ $selected_bundles = [];
if ($entity_type->hasKey('bundle')) {
$bundle_options = [];
@@ -200,6 +201,7 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
'class' => ['js-hide'],
],
'#submit' => [[EntityReferenceItem::class, 'settingsAjaxSubmit']],
+ '#element_validate' => [[static::class, 'validateTargetBundlesUpdate']],
];
}
else {
@@ -208,6 +210,7 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
'#value' => [],
];
}
+ $form['target_bundles']['#element_validate'][] = [static::class, 'validateTargetBundles'];
if ($entity_type->entityClassImplements(FieldableEntityInterface::class)) {
$options = $entity_type->hasKey('bundle') ? $selected_bundles : $bundles;
@@ -224,7 +227,11 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
// @todo Use property labels instead of the column name.
if (count($columns) > 1) {
foreach ($columns as $column_name => $column_info) {
- $fields[$field_name . '.' . $column_name] = $this->t('@label (@column)', ['@label' => $field_definition->getLabel(), '@column' => $column_name]);
+ $fields[$field_name . '.' . $column_name] = $this->t('@label (@column)',
+ [
+ '@label' => $field_definition->getLabel(),
+ '@column' => $column_name,
+ ]);
}
}
else {
@@ -314,22 +321,25 @@ public function buildConfigurationForm(array $form, FormStateInterface $form_sta
}
/**
- * {@inheritdoc}
+ * Validates a target_bundles element.
*/
- public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
- parent::validateConfigurationForm($form, $form_state);
-
+ public static function validateTargetBundles($element, FormStateInterface $form_state, $form) {
// If no checkboxes were checked for 'target_bundles', store NULL ("all
// bundles are referenceable") rather than empty array ("no bundle is
// referenceable" - typically happens when all referenceable bundles have
// been deleted).
- if ($form_state->getValue(['settings', 'handler_settings', 'target_bundles']) === []) {
- $form_state->setValue(['settings', 'handler_settings', 'target_bundles'], NULL);
+ if ($form_state->getValue($element['#parents']) === []) {
+ $form_state->setValueForElement($element, NULL);
}
+ }
+ /**
+ * Validates a target_bundles_update element.
+ */
+ public static function validateTargetBundlesUpdate($element, FormStateInterface $form_state, $form) {
// Don't store the 'target_bundles_update' button value into the field
// config settings.
- $form_state->unsetValue(['settings', 'handler_settings', 'target_bundles_update']);
+ $form_state->unsetValue($element['#parents']);
}
/**
diff --git a/core/modules/views/config/schema/views.filter.schema.yml b/core/modules/views/config/schema/views.filter.schema.yml
index 1eb09007aef9132ece61fb2ae58e25090ad5eb4f..6eb22b82f996175cba2ba2c1aad00048cc11019b 100644
--- a/core/modules/views/config/schema/views.filter.schema.yml
+++ b/core/modules/views/config/schema/views.filter.schema.yml
@@ -128,6 +128,26 @@ views.filter.many_to_one:
type: boolean
label: 'Reduce duplicate'
+views.filter.entity_reference:
+ type: views.filter.many_to_one
+ label: 'Entity reference'
+ constraints:
+ FullyValidatable: ~
+ mapping:
+ sub_handler:
+ type: string
+ label: 'Selection handler'
+ constraints:
+ PluginExists:
+ manager: plugin.manager.entity_reference_selection
+ interface: 'Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface'
+ widget:
+ type: string
+ label: 'Selection type'
+ sub_handler_settings:
+ type: entity_reference_selection.[%parent.sub_handler]
+ 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 0000000000000000000000000000000000000000..83e9f95ce163cb381d77e1e4af0fdfe200143f77
--- /dev/null
+++ b/core/modules/views/src/Plugin/views/filter/EntityReference.php
@@ -0,0 +1,704 @@
+<?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\EntityReferenceSelection\ViewsSelection;
+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';
+
+ /**
+ * Max number of entities in the select widget.
+ */
+ const WIDGET_SELECT_LIMIT = 100;
+
+ /**
+ * The subform prefix.
+ */
+ const SUBFORM_PREFIX = 'reference_';
+
+ /**
+ * The all value.
+ */
+ const ALL_VALUE = 'All';
+
+ /**
+ * The selection handlers available for the target entity ID of the filter.
+ *
+ * @var array|null
+ */
+ protected ?array $handlerOptions = NULL;
+
+ /**
+ * Validated exposed input that will be set as the input value.
+ *
+ * If the select list widget is chosen.
+ *
+ * @var array
+ */
+ protected array $validatedExposedInput;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function init(ViewExecutable $view, DisplayPluginBase $display, ?array &$options = NULL): void {
+ 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($this->options['sub_handler'])];
+ }
+
+ /**
+ * Constructs an EntityReference object.
+ */
+ public function __construct(
+ array $configuration,
+ $plugin_id,
+ $plugin_definition,
+ protected SelectionPluginManagerInterface $selectionPluginManager,
+ protected EntityTypeManagerInterface $entityTypeManager,
+ MessengerInterface $messenger,
+ ) {
+ parent::__construct($configuration, $plugin_id, $plugin_definition);
+ $this->setMessenger($messenger);
+
+ // @todo Unify 'entity field'/'field_name' instead of converting back and
+ // forth. https://www.drupal.org/node/2410779
+ if (isset($this->definition['entity field'])) {
+ $this->definition['field_name'] = $this->definition['entity field'];
+ }
+ }
+
+ /**
+ * {@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'),
+ );
+ }
+
+ /**
+ * Gets the entity reference selection handler.
+ *
+ * @param string|null $sub_handler
+ * The sub handler to get an instance of or NULL for the current selection.
+ *
+ * @return \Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface
+ * The selection handler plugin instance.
+ */
+ protected function getSelectionHandler(?string $sub_handler = NULL): SelectionInterface {
+ // Default values for the handler.
+ $handler_settings = $this->options['sub_handler_settings'] ?? [];
+ $handler_settings['handler'] = $sub_handler;
+ $handler_settings['target_type'] = $this->getReferencedEntityType()->id();
+ /** @var \Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface */
+ return $this->selectionPluginManager->getInstance($handler_settings);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function defineOptions(): array {
+ $options = parent::defineOptions();
+ $options['sub_handler'] = [
+ 'default' => 'default:' . $this->getReferencedEntityType()->id(),
+ ];
+ $options['sub_handler_settings'] = ['default' => []];
+ $options['widget'] = ['default' => static::WIDGET_AUTOCOMPLETE];
+ return $options;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hasExtraOptions(): bool {
+ return TRUE;
+ }
+
+ /**
+ * Get all selection plugins for this entity type.
+ *
+ * @return string[]
+ * The selection handlers available for the target entity ID of the filter.
+ */
+ protected function getSubHandlerOptions(): array {
+ if ($this->handlerOptions) {
+ return $this->handlerOptions;
+ }
+ $entity_type = $this->getReferencedEntityType();
+ $selection_plugins = $this->selectionPluginManager->getSelectionGroups($entity_type->id());
+ $this->handlerOptions = [];
+ foreach (array_keys($selection_plugins) as $selection_group_id) {
+ // We only display base plugins (e.g. 'default', 'views', ...).
+ if (array_key_exists($selection_group_id, $selection_plugins[$selection_group_id])) {
+ $this->handlerOptions[$selection_group_id] = (string) $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();
+ $this->handlerOptions[$selection_group_plugin] = (string) $selection_plugins[$selection_group_id][$selection_group_plugin]['base_plugin_label'];
+ }
+ }
+ return $this->handlerOptions;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildExtraOptionsForm(&$form, FormStateInterface $form_state): void {
+ $form['sub_handler'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Reference method'),
+ '#options' => $this->getSubHandlerOptions(),
+ '#default_value' => $this->options['sub_handler'],
+ '#required' => TRUE,
+ ];
+
+ // We store the settings from any sub handler in sub_handler_settings, but
+ // in this form, we have multiple sub handlers conditionally displayed.
+ // Copy the active sub_handler_settings into the handler specific settings
+ // to set the defaults to match the saved options on build.
+ if (!empty($this->options['sub_handler']) && !empty($this->options['sub_handler_settings'])) {
+ $this->options[static::SUBFORM_PREFIX . $this->options['sub_handler']] = $this->options['sub_handler_settings'];
+ }
+
+ foreach ($this->getSubHandlerOptions() as $sub_handler => $sub_handler_label) {
+ $subform_key = static::SUBFORM_PREFIX . $sub_handler;
+ $subform = [
+ '#type' => 'fieldset',
+ '#title' => $this->t('Reference type "@type"', [
+ '@type' => $sub_handler_label,
+ ]),
+ '#tree' => TRUE,
+ '#parents' => [
+ 'options',
+ $subform_key,
+ ],
+ // Make the sub handler settings conditional on the selected selection
+ // handler.
+ '#states' => [
+ 'visible' => [
+ 'select[name="options[sub_handler]"]' => ['value' => $sub_handler],
+ ],
+ ],
+ ];
+
+ // Build the sub form and sub for state.
+ $selection_handler = $this->getSelectionHandler($sub_handler);
+ if (!empty($this->options[$subform_key])) {
+ $selection_config = $selection_handler->getConfiguration();
+ $selection_config = NestedArray::mergeDeepArray([
+ $selection_config,
+ $this->options[$subform_key],
+ ], TRUE);
+ $selection_handler->setConfiguration($selection_config);
+ }
+ $subform_state = SubformState::createForSubform($subform, $form, $form_state);
+ $sub_handler_settings = $selection_handler->buildConfigurationForm($subform, $subform_state);
+
+ if ($selection_handler instanceof ViewsSelection) {
+ if (isset($sub_handler_settings['view']['no_view_help'])) {
+ // If there are no views with entity reference displays,
+ // ViewsSelection still validates the view.
+ // This will prevent form config extra form submission,
+ // so we remove it here.
+ unset($sub_handler_settings['view']['#element_validate']);
+ }
+ }
+ else {
+ // Remove unnecessary and inappropriate handler settings from the
+ // filter config form.
+ $sub_handler_settings['target_bundles_update']['#access'] = FALSE;
+ $sub_handler_settings['auto_create']['#access'] = FALSE;
+ $sub_handler_settings['auto_create_bundle']['#access'] = FALSE;
+ }
+
+ $subform = NestedArray::mergeDeepArray([
+ $subform,
+ $sub_handler_settings,
+ ], TRUE);
+
+ $form[$subform_key] = $subform;
+ $this->cleanUpSubformChildren($form[$subform_key]);
+ }
+
+ $form['widget'] = [
+ '#type' => 'radios',
+ '#title' => $this->t('Selection type'),
+ '#default_value' => $this->options['widget'],
+ '#options' => [
+ static::WIDGET_SELECT => $this->t('Select list'),
+ static::WIDGET_AUTOCOMPLETE => $this->t('Autocomplete'),
+ ],
+ '#description' => $this->t('For performance and UX reasons, the maximum count of selectable entities for the "Select list" selection type is limited to @count. If more is expected, select "Autocomplete" instead.', [
+ '@count' => static::WIDGET_SELECT_LIMIT,
+ ]),
+ ];
+ }
+
+ /**
+ * Clean up subform children for properties that could cause problems.
+ *
+ * Views modal forms do not work with required or ajax elements.
+ *
+ * @param array $element
+ * The form element.
+ */
+ protected function cleanUpSubformChildren(array &$element): void {
+ // Remove the required property to prevent focus errors.
+ if (isset($element['#required']) && $element['#required']) {
+ $element['#required'] = FALSE;
+ $element['#element_validate'][] = [static::class, 'validateRequired'];
+ }
+
+ // Remove the ajax property as it does not work.
+ if (!empty($element['#ajax'])) {
+ unset($element['#ajax']);
+ }
+
+ // Recursively apply to nested fields within the handler sub form.
+ foreach (Element::children($element) as $delta) {
+ $this->cleanUpSubformChildren($element[$delta]);
+ }
+ }
+
+ /**
+ * Validates that a required field for a sub handler has a value.
+ *
+ * @param array $element
+ * The cardinality form render array.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The form state.
+ */
+ public static function validateRequired(array &$element, FormStateInterface $form_state): void {
+ if (!empty($element['value'])) {
+ return;
+ }
+
+ // Config extra handler does not output validation messages and
+ // closes the modal with no feedback to the user.
+ // @todo https://www.drupal.org/project/drupal/issues/3163740.
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateExtraOptionsForm($form, FormStateInterface $form_state): void {
+ $options = $form_state->getValue('options');
+ $sub_handler = $options['sub_handler'];
+ $subform = $form[static::SUBFORM_PREFIX . $sub_handler];
+ $subform_state = SubformState::createForSubform($subform, $form, $form_state);
+
+ // Copy handler_settings from options to settings to be compatible with
+ // selection plugins.
+ $subform_options = $form_state->getValue([
+ 'options',
+ static::SUBFORM_PREFIX . $sub_handler,
+ ]);
+ $subform_state->setValue([
+ 'settings',
+ ], $subform_options);
+ $this->getSelectionHandler($sub_handler)
+ ->validateConfigurationForm($subform, $subform_state);
+
+ // Store the sub handler options in sub_handler_settings.
+ $form_state->setValue(['options', 'sub_handler_settings'], $subform_options);
+
+ // Remove options that are not from the selected sub_handler.
+ foreach (array_keys($this->getSubHandlerOptions()) as $sub_handler_option) {
+ if (isset($options[static::SUBFORM_PREFIX . $sub_handler_option])) {
+ $form_state->unsetValue(['options', static::SUBFORM_PREFIX . $sub_handler_option]);
+ }
+ }
+
+ parent::validateExtraOptionsForm($form, $form_state);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitExtraOptionsForm($form, FormStateInterface $form_state): void {
+ $sub_handler = $form_state->getValue('options')['sub_handler'];
+
+ // Ensure that only the select sub handler option is saved.
+ foreach (array_keys($this->getSubHandlerOptions()) as $sub_handler_option) {
+ if ($sub_handler_option == $sub_handler) {
+ $this->options['sub_handler_settings'] = $this->options[static::SUBFORM_PREFIX . $sub_handler_option];
+ }
+ if (isset($this->options[static::SUBFORM_PREFIX . $sub_handler_option])) {
+ unset($this->options[static::SUBFORM_PREFIX . $sub_handler_option]);
+ }
+ }
+ }
+
+ /**
+ * 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): void {
+ $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) {
+ if (!isset($this->options['sub_handler'])) {
+ return;
+ }
+ switch ($this->options['widget']) {
+ case static::WIDGET_SELECT:
+ $this->valueFormAddSelect($form, $form_state);
+ break;
+
+ case static::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 = $this->options['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".');
+ }
+ }
+
+ /**
+ * Adds 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): void {
+ $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' => $referenced_type->id(),
+ '#selection_handler' => $this->options['sub_handler'],
+ '#selection_settings' => $this->options['sub_handler_settings'],
+ // Validation is done by validateExposed().
+ '#validate_reference' => FALSE,
+ ];
+ }
+
+ /**
+ * Adds 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): void {
+ $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 = static::ALL_VALUE;
+ }
+ elseif (empty($default_value)) {
+ $keys = array_keys($options);
+ $default_value = array_shift($keys);
+ }
+ else {
+ // Set the default value to be the first element of the array.
+ $default_value = reset($default_value);
+ }
+ }
+ }
+
+ $referenced_type = $this->getReferencedEntityType();
+ $form['value'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Select @entity_types', ['@entity_types' => $referenced_type->getPluralLabel()]),
+ '#multiple' => TRUE,
+ '#options' => $options,
+ // Set a minimum size to facilitate easier selection of entities.
+ '#size' => min(8, 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 !empty($this->value) && !isset($this->value[static::ALL_VALUE]) ? $entity_storage->loadMultiple($this->value) : [];
+ }
+
+ /**
+ * Returns the value options for a select widget.
+ *
+ * @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 {
+ $entity_data = [];
+ if ($this->options['widget'] === static::WIDGET_SELECT) {
+ $entity_data = $selection_handler->getReferenceableEntities(NULL, 'CONTAINS', static::WIDGET_SELECT_LIMIT);
+ }
+
+ $options = [];
+ foreach ($entity_data as $bundle) {
+ foreach ($bundle as $id => $entity_label) {
+ $options[$id] = $entity_label;
+ }
+ }
+
+ return $options;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validate(): array {
+ // InOperator validation logic is not appropriate for entity reference
+ // autocomplete or select, so prevent parent class validation from
+ // occurring.
+ return [];
+ }
+
+ /**
+ * {@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;
+ }
+
+ $accept_exposed_input = parent::acceptExposedInput($input);
+ if ($accept_exposed_input) {
+ // If we have previously validated input, override.
+ if (isset($this->validatedExposedInput)) {
+ $this->value = $this->validatedExposedInput;
+ }
+ }
+
+ return $accept_exposed_input;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateExposed(&$form, FormStateInterface $form_state): void {
+ if (empty($this->options['exposed'])) {
+ return;
+ }
+
+ $identifier = $this->options['expose']['identifier'];
+
+ // Set the validated exposed input from the select list when not the all
+ // value option.
+ if ($this->options['widget'] == static::WIDGET_SELECT) {
+ if ($form_state->getValue($identifier) != static::ALL_VALUE) {
+ $this->validatedExposedInput = (array) $form_state->getValue($identifier);
+ }
+ return;
+ }
+
+ if (empty($identifier)) {
+ return;
+ }
+
+ $values = $form_state->getValue($identifier);
+ if (!is_array($values)) {
+ return;
+ }
+
+ foreach ($values as $value) {
+ $this->validatedExposedInput[] = $value['target_id'];
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function valueSubmit($form, FormStateInterface $form_state): void {
+ // Prevent the parent class InOperator from altering the array.
+ // @see \Drupal\views\Plugin\views\filter\InOperator::valueSubmit().
+ }
+
+ /**
+ * Gets the target entity type referenced by this field.
+ *
+ * @return \Drupal\Core\Entity\EntityTypeInterface
+ * The entity type definition.
+ */
+ 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();
+
+ $sub_handler = $this->options['sub_handler'];
+ $selection_handler = $this->getSelectionHandler($sub_handler);
+ 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 0000000000000000000000000000000000000000..5a15e68ed0c8c48c9c9b5cff9bcf32789e9a8b78
--- /dev/null
+++ b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_filter_entity_reference.yml
@@ -0,0 +1,268 @@
+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
+ sub_handler: 'default:node'
+ sub_handler_settings:
+ target_bundles:
+ article: article
+ sort:
+ field: title
+ direction: ASC
+ auto_create: false
+ auto_create_bundle: ''
+ widget: select
+ plugin_id: entity_reference
+ field_test_config_target_id:
+ id: field_test_config_target_id
+ table: node__field_test_config
+ field: field_test_config_target_id
+ relationship: none
+ group_type: group
+ admin_label: ''
+ plugin_id: entity_reference
+ operator: or
+ value: { }
+ group: 1
+ exposed: true
+ expose:
+ operator_id: field_test_config_target_id_op
+ label: 'Test config (field_test_config)'
+ description: ''
+ use_operator: false
+ operator: field_test_config_target_id_op
+ operator_limit_selection: false
+ operator_list: { }
+ identifier: field_test_config_target_id
+ required: false
+ remember: false
+ multiple: false
+ remember_roles:
+ authenticated: authenticated
+ anonymous: '0'
+ content_editor: '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
+ sub_handler: 'default:node_type'
+ widget: select
+ sub_handler_settings:
+ target_bundles: null
+ auto_create: false
+ 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/tests/modules/views_test_entity_reference/config/install/views.view.test_entity_reference.yml b/core/modules/views/tests/modules/views_test_entity_reference/config/install/views.view.test_entity_reference.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4b37d55d492dcbcffeb88357b6cac472eca7bf45
--- /dev/null
+++ b/core/modules/views/tests/modules/views_test_entity_reference/config/install/views.view.test_entity_reference.yml
@@ -0,0 +1,269 @@
+langcode: en
+status: true
+dependencies:
+ module:
+ - node
+ - user
+id: test_entity_reference
+label: 'Test Entity Reference'
+module: views
+description: ''
+tag: ''
+base_table: node_field_data
+base_field: nid
+display:
+ default:
+ id: default
+ display_title: Default
+ display_plugin: default
+ position: 0
+ display_options:
+ fields:
+ title:
+ id: title
+ table: node_field_data
+ field: title
+ relationship: none
+ group_type: group
+ admin_label: ''
+ entity_type: node
+ entity_field: title
+ plugin_id: field
+ label: ''
+ exclude: false
+ alter:
+ alter_text: false
+ text: ''
+ make_link: false
+ path: ''
+ absolute: false
+ external: false
+ replace_spaces: false
+ path_case: none
+ trim_whitespace: false
+ alt: ''
+ rel: ''
+ link_class: ''
+ prefix: ''
+ suffix: ''
+ target: ''
+ nl2br: false
+ max_length: 0
+ word_boundary: false
+ ellipsis: false
+ more_link: false
+ more_link_text: ''
+ more_link_path: ''
+ strip_tags: false
+ trim: false
+ preserve_tags: ''
+ html: false
+ element_type: ''
+ element_class: ''
+ element_label_type: ''
+ element_label_class: ''
+ element_label_colon: false
+ element_wrapper_type: ''
+ element_wrapper_class: ''
+ element_default_classes: true
+ empty: ''
+ hide_empty: false
+ empty_zero: false
+ hide_alter_empty: true
+ click_sort_column: value
+ type: string
+ settings:
+ link_to_entity: true
+ 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
+ pager:
+ type: mini
+ options:
+ offset: 0
+ items_per_page: 10
+ total_pages: null
+ id: 0
+ tags:
+ next: ››
+ previous: ‹‹
+ 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
+ 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
+ access:
+ type: perm
+ options:
+ perm: 'access content'
+ cache:
+ type: tag
+ options: { }
+ empty: { }
+ sorts:
+ created:
+ id: created
+ table: node_field_data
+ field: created
+ relationship: none
+ group_type: group
+ admin_label: ''
+ entity_type: node
+ entity_field: created
+ plugin_id: date
+ order: DESC
+ expose:
+ label: ''
+ field_identifier: ''
+ exposed: false
+ granularity: second
+ arguments: { }
+ filters:
+ status:
+ id: status
+ table: node_field_data
+ field: status
+ entity_type: node
+ entity_field: status
+ plugin_id: boolean
+ value: '1'
+ group: 1
+ expose:
+ operator: ''
+ operator_limit_selection: false
+ operator_list: { }
+ type:
+ id: type
+ table: node_field_data
+ field: type
+ entity_type: node
+ entity_field: type
+ plugin_id: bundle
+ value:
+ article: article
+ expose:
+ operator_limit_selection: false
+ operator_list: { }
+ title:
+ id: title
+ table: node_field_data
+ field: title
+ relationship: none
+ group_type: group
+ admin_label: ''
+ entity_type: node
+ entity_field: title
+ plugin_id: string
+ operator: '='
+ value: 'Article 0'
+ group: 1
+ exposed: false
+ expose:
+ operator_id: ''
+ label: ''
+ description: ''
+ use_operator: false
+ operator: ''
+ operator_limit_selection: false
+ operator_list: { }
+ identifier: ''
+ required: false
+ remember: false
+ multiple: false
+ remember_roles:
+ authenticated: authenticated
+ placeholder: ''
+ is_grouped: false
+ group_info:
+ label: ''
+ description: ''
+ identifier: ''
+ optional: true
+ widget: select
+ multiple: false
+ remember: false
+ default_group: All
+ default_group_multiple: { }
+ group_items: { }
+ style:
+ type: default
+ options:
+ grouping: { }
+ row_class: ''
+ default_row_class: true
+ uses_fields: false
+ row:
+ type: fields
+ options:
+ default_field_elements: true
+ inline: { }
+ separator: ''
+ hide_empty: false
+ query:
+ type: views_query
+ options:
+ query_comment: ''
+ disable_sql_rewrite: false
+ distinct: false
+ replica: false
+ query_tags: { }
+ relationships: { }
+ header: { }
+ footer: { }
+ display_extenders: { }
+ cache_metadata:
+ max-age: -1
+ contexts:
+ - 'languages:language_content'
+ - 'languages:language_interface'
+ - url.query_args
+ - 'user.node_grants:view'
+ - user.permissions
+ tags: { }
+ entity_reference:
+ id: entity_reference
+ display_title: 'Entity Reference'
+ display_plugin: entity_reference
+ position: 1
+ display_options:
+ style:
+ type: entity_reference
+ options:
+ search_fields:
+ title: title
+ row:
+ type: entity_reference
+ options:
+ default_field_elements: true
+ inline: { }
+ separator: '-'
+ hide_empty: false
+ display_extenders: { }
+ cache_metadata:
+ max-age: -1
+ contexts:
+ - 'languages:language_content'
+ - 'languages:language_interface'
+ - 'user.node_grants:view'
+ - user.permissions
+ tags: { }
diff --git a/core/modules/views/tests/modules/views_test_entity_reference/views_test_entity_reference.info.yml b/core/modules/views/tests/modules/views_test_entity_reference/views_test_entity_reference.info.yml
new file mode 100644
index 0000000000000000000000000000000000000000..3bc975ea74781039f3cdb9bfe5395688c89387d3
--- /dev/null
+++ b/core/modules/views/tests/modules/views_test_entity_reference/views_test_entity_reference.info.yml
@@ -0,0 +1,8 @@
+name: 'Views Test Entity Reference'
+type: module
+description: 'Provides an entity reference view for use in a selection handler.'
+package: Testing
+version: VERSION
+dependencies:
+ - drupal:views
+ - drupal:field
diff --git a/core/modules/views/tests/modules/views_test_entity_reference/views_test_entity_reference.module b/core/modules/views/tests/modules/views_test_entity_reference/views_test_entity_reference.module
new file mode 100644
index 0000000000000000000000000000000000000000..248a96757537291d4bbe6213f4c0cd6ffe5903d5
--- /dev/null
+++ b/core/modules/views/tests/modules/views_test_entity_reference/views_test_entity_reference.module
@@ -0,0 +1,28 @@
+<?php
+
+/**
+ * @file
+ * Views data altering to test use of the entity reference plugin.
+ */
+
+/**
+ * Implements hook_views_data_alter().
+ */
+function views_test_entity_reference_views_data_alter(&$data) {
+ $manager = \Drupal::entityTypeManager();
+ $field_config_storage = $manager->getStorage('field_config');
+ /** @var \Drupal\field\FieldConfigInterface[] $field_configs */
+ $field_configs = $field_config_storage->loadByProperties([
+ 'field_type' => 'entity_reference',
+ ]);
+ foreach ($field_configs as $field_config) {
+ $table_name = $field_config->getTargetEntityTypeId() . '__' . $field_config->getName();
+ $column_name = $field_config->getName() . '_target_id';
+ if (
+ isset($data[$table_name][$column_name]['filter']['id'])
+ && in_array($data[$table_name][$column_name]['filter']['id'], ['numeric', 'string'])
+ ) {
+ $data[$table_name][$column_name]['filter']['id'] = 'entity_reference';
+ }
+ }
+}
diff --git a/core/modules/views/tests/src/Kernel/Handler/FilterEntityReferenceTest.php b/core/modules/views/tests/src/Kernel/Handler/FilterEntityReferenceTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..1fbf7cf3a9b46e42dff79927e309e56ec129d2aa
--- /dev/null
+++ b/core/modules/views/tests/src/Kernel/Handler/FilterEntityReferenceTest.php
@@ -0,0 +1,256 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\views\Kernel\Handler;
+
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Tests\field\Traits\EntityReferenceFieldCreationTrait;
+use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
+use Drupal\Tests\node\Traits\NodeCreationTrait;
+use Drupal\Tests\user\Traits\UserCreationTrait;
+use Drupal\Tests\views\Kernel\ViewsKernelTestBase;
+use Drupal\user\UserInterface;
+use Drupal\views\Plugin\views\filter\EntityReference;
+use Drupal\views\Tests\ViewTestData;
+use Drupal\views\Views;
+
+/**
+ * Tests the core Drupal\views\Plugin\views\filter\EntityReference handler.
+ *
+ * @group views
+ */
+class FilterEntityReferenceTest extends ViewsKernelTestBase {
+
+ use ContentTypeCreationTrait;
+ use EntityReferenceFieldCreationTrait;
+ use NodeCreationTrait;
+ use UserCreationTrait;
+
+ /**
+ * {@inheritdoc}
+ */
+ public static $testViews = ['test_filter_entity_reference'];
+
+ /**
+ * {@inheritdoc}
+ */
+ protected static $modules = [
+ 'system',
+ 'node',
+ 'user',
+ 'field',
+ 'text',
+ 'filter',
+ 'views',
+ 'views_test_entity_reference',
+ ];
+
+ /**
+ * Test host nodes containing the entity reference.
+ *
+ * @var \Drupal\node\NodeInterface[]
+ */
+ protected array $hostNodes;
+
+ /**
+ * Test target nodes referenced by the entity reference.
+ *
+ * @var \Drupal\node\NodeInterface[]
+ */
+ protected array $targetNodes;
+
+ /**
+ * First test user as node author.
+ *
+ * @var \Drupal\user\UserInterface
+ */
+ protected UserInterface $user1;
+
+ /**
+ * Second test user as node author.
+ *
+ * @var \Drupal\user\UserInterface
+ */
+ protected UserInterface $user2;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp($import_test_views = TRUE): void {
+ parent::setUp(FALSE);
+ $this->installEntitySchema('node');
+ $this->installEntitySchema('user');
+ $this->installConfig(['node', 'user', 'filter']);
+
+ ViewTestData::createTestViews(static::class, ['views_test_config']);
+ // Create two node types.
+ $this->createContentType(['type' => 'page']);
+ $this->createContentType(['type' => 'article']);
+
+ // Add an entity reference field to the page type referencing the article
+ // type.
+ $selection_handler_settings = [
+ 'target_bundles' => [
+ 'article' => 'article',
+ ],
+ ];
+ $this->createEntityReferenceField('node', 'page', 'field_test', 'Test reference', 'node', $selection_handler = 'default', $selection_handler_settings, FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
+
+ // Create user 1.
+ $this->user1 = $this->createUser();
+ $this->user2 = $this->createUser();
+
+ // Create target nodes to be referenced.
+ foreach (range(0, 5) as $count) {
+ $this->targetNodes[$count] = $this->createNode([
+ 'type' => 'article',
+ 'title' => 'Article ' . $count,
+ 'status' => 1,
+ 'uid' => $this->user1,
+ ]);
+ }
+
+ // Create a page referencing Article 0 and Article 1.
+ $this->hostNodes[0] = $this->createNode([
+ 'type' => 'page',
+ 'title' => 'Page 0',
+ 'status' => 1,
+ 'created' => time(),
+ 'field_test' => [
+ $this->targetNodes[0]->id(),
+ $this->targetNodes[1]->id(),
+ ],
+ 'uid' => $this->user2,
+ ]);
+
+ // Create a page referencing Article 1, Article 2, and Article 3.
+ $this->hostNodes[1] = $this->createNode([
+ 'type' => 'page',
+ 'title' => 'Page 1',
+ 'status' => 1,
+ 'created' => time() - 100,
+ 'field_test' => [
+ $this->targetNodes[1]->id(),
+ $this->targetNodes[2]->id(),
+ $this->targetNodes[3]->id(),
+ ],
+ 'uid' => $this->user2,
+ ]);
+
+ // Create a page referencing nothing.
+ $this->hostNodes[2] = $this->createNode([
+ 'type' => 'page',
+ 'title' => 'Page 2',
+ 'status' => 1,
+ 'created' => time() - 200,
+ 'uid' => $this->user2,
+ ]);
+ }
+
+ /**
+ * Tests that results are successfully filtered by the select list widget.
+ */
+ public function testViewEntityReferenceAsSelectList(): void {
+ $view = Views::getView('test_filter_entity_reference');
+ $view->setDisplay();
+ $view->preExecute([]);
+ $view->setExposedInput([
+ 'field_test_target_id' => [$this->targetNodes[0]->id()],
+ ]);
+ $this->executeView($view);
+
+ // Expect to have only Page 0, with Article 0 referenced.
+ $expected = [
+ ['title' => 'Page 0'],
+ ];
+ $this->assertIdenticalResultset($view, $expected, [
+ 'title' => 'title',
+ ]);
+
+ // Change to both Article 0 and Article 3.
+ $view = Views::getView('test_filter_entity_reference');
+ $view->setDisplay();
+ $view->setExposedInput([
+ 'field_test_target_id' => [
+ $this->targetNodes[0]->id(),
+ $this->targetNodes[3]->id(),
+ ],
+ ]);
+ $this->executeView($view);
+
+ // Expect to have Page 0 and 1, with Article 0 and 3 referenced.
+ $expected = [
+ ['title' => 'Page 0'],
+ ['title' => 'Page 1'],
+ ];
+ $this->assertIdenticalResultset($view, $expected, [
+ 'title' => 'title',
+ ]);
+ }
+
+ /**
+ * Tests that results are successfully filtered by the autocomplete widget.
+ */
+ public function testViewEntityReferenceAsAutocomplete(): void {
+ // Change the widget to autocomplete.
+ $view = Views::getView('test_filter_entity_reference');
+ $view->setDisplay();
+ $filters = $view->displayHandlers->get('default')->getOption('filters');
+ $filters['field_test_target_id']['widget'] = EntityReference::WIDGET_AUTOCOMPLETE;
+ $view->displayHandlers->get('default')->overrideOption('filters', $filters);
+ $view->setExposedInput([
+ 'field_test_target_id' => [
+ ['target_id' => $this->targetNodes[0]->id()],
+ ['target_id' => $this->targetNodes[3]->id()],
+ ],
+ ]);
+ $this->executeView($view);
+
+ // Expect to have Page 0 and 1, with Article 0 and 3 referenced.
+ $expected = [
+ ['title' => 'Page 0'],
+ ['title' => 'Page 1'],
+ ];
+ $this->assertIdenticalResultset($view, $expected, [
+ 'title' => 'title',
+ ]);
+ }
+
+ /**
+ * Tests that content dependencies are added to the view.
+ */
+ public function testViewContentDependencies(): void {
+ $view = Views::getView('test_filter_entity_reference');
+ $value = [
+ $this->targetNodes[0]->id(),
+ $this->targetNodes[3]->id(),
+ ];
+ $view->setHandlerOption(
+ 'default',
+ 'filter',
+ 'field_test_target_id',
+ 'value',
+ $value
+ );
+
+ // Dependencies are sorted.
+ $content_dependencies = [
+ $this->targetNodes[0]->getConfigDependencyName(),
+ $this->targetNodes[3]->getConfigDependencyName(),
+ ];
+ sort($content_dependencies);
+
+ $this->assertEquals([
+ 'config' => [
+ 'node.type.page',
+ ],
+ 'content' => $content_dependencies,
+ 'module' => [
+ 'node',
+ 'user',
+ ],
+ ], $view->getDependencies());
+ }
+
+}
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 0000000000000000000000000000000000000000..74cadc64d628f51169ed6f4fb2ab825badf7f12c
--- /dev/null
+++ b/core/modules/views_ui/tests/src/Functional/FilterEntityReferenceWebTest.php
@@ -0,0 +1,138 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\views_ui\Functional;
+
+use Drupal\Component\Render\FormattableMarkup;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Tests\views_ui\Traits\FilterEntityReferenceTrait;
+
+/**
+ * Tests the entity reference filter UI.
+ *
+ * @group views_ui
+ * @see \Drupal\views\Plugin\views\filter\EntityReference
+ */
+class FilterEntityReferenceWebTest extends UITestBase {
+
+ use FilterEntityReferenceTrait;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $defaultTheme = 'stark';
+
+ /**
+ * {@inheritdoc}
+ */
+ public static $testViews = ['test_filter_entity_reference'];
+
+ /**
+ * {@inheritdoc}
+ */
+ protected static $modules = [
+ 'node',
+ 'views_ui',
+ 'block',
+ 'taxonomy',
+ 'views_test_entity_reference',
+ ];
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp($import_test_views = TRUE, $modules = []): void {
+ parent::setUp($import_test_views);
+ $this->setUpEntityTypes();
+ }
+
+ /**
+ * Tests the filter UI.
+ */
+ public function testFilterUi(): void {
+ $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->targetEntities, function (EntityInterface $a, EntityInterface $b) {
+ return strnatcasecmp($a->getTitle(), $b->getTitle());
+ });
+ $i = 0;
+ foreach ($this->targetEntities as $id => $entity) {
+ $message = (string) new FormattableMarkup('Expected target entity label found for option :option', [':option' => $i]);
+ $this->assertEquals($options[$i]['label'], $entity->label(), $message);
+ $i++;
+ }
+
+ // Change the sort field and direction.
+ $this->drupalGet('admin/structure/views/nojs/handler-extra/test_filter_entity_reference/default/filter/field_test_target_id');
+ $edit = [
+ 'options[reference_default:node][sort][field]' => 'nid',
+ 'options[reference_default:node][sort][direction]' => 'DESC',
+ ];
+ $this->submitForm($edit, 'Apply');
+
+ $this->drupalGet('admin/structure/views/nojs/handler/test_filter_entity_reference/default/filter/field_test_target_id');
+ // Items should now be in reverse id order.
+ krsort($this->targetEntities);
+ $options = $this->getUiOptions();
+ $i = 0;
+ foreach ($this->targetEntities as $entity) {
+ $message = (string) new FormattableMarkup('Expected target entity label found for option :option', [':option' => $i]);
+ $this->assertEquals($options[$i]['label'], $entity->label(), $message);
+ $i++;
+ }
+
+ // Change bundle types.
+ $this->drupalGet('admin/structure/views/nojs/handler-extra/test_filter_entity_reference/default/filter/field_test_target_id');
+ $edit = [
+ "options[reference_default:node][target_bundles][{$this->hostBundle->id()}]" => TRUE,
+ "options[reference_default:node][target_bundles][{$this->targetBundle->id()}]" => TRUE,
+ ];
+ $this->submitForm($edit, 'Apply');
+
+ $this->drupalGet('admin/structure/views/nojs/handler/test_filter_entity_reference/default/filter/field_test_target_id');
+ $options = $this->getUiOptions();
+ $i = 0;
+ foreach ($this->hostEntities + $this->targetEntities as $entity) {
+ $message = (string) new FormattableMarkup('Expected target entity label found for option :option', [':option' => $i]);
+ $this->assertEquals($options[$i]['label'], $entity->label(), $message);
+ $i++;
+ }
+ }
+
+ /**
+ * Tests the filter UI for config reference.
+ */
+ public function testFilterConfigUi(): void {
+ $this->drupalGet('admin/structure/views/nojs/handler/test_filter_entity_reference/default/filter/field_test_config_target_id');
+
+ $options = $this->getUiOptions();
+ // We should expect the content types defined as options.
+ $this->assertEquals(['article', 'page'], array_column($options, 'label'));
+ }
+
+ /**
+ * Helper method to parse options from the UI.
+ *
+ * @return array
+ * Array of keyed arrays containing the id and label of each option.
+ */
+ protected function getUiOptions(): array {
+ /** @var \Behat\Mink\Element\TraversableElement[] $result */
+ $result = $this->xpath('//select[@name="options[value][]"]/option');
+ $this->assertNotEmpty($result, 'Options found');
+
+ $options = [];
+ foreach ($result as $option) {
+ $options[] = [
+ 'id' => (int) $option->getValue(),
+ 'label' => $option->getText(),
+ ];
+ }
+
+ return $options;
+ }
+
+}
diff --git a/core/modules/views_ui/tests/src/FunctionalJavascript/FilterEntityReferenceTest.php b/core/modules/views_ui/tests/src/FunctionalJavascript/FilterEntityReferenceTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..b0621fccd0f1f6502a93808ae4f5bbc687150d44
--- /dev/null
+++ b/core/modules/views_ui/tests/src/FunctionalJavascript/FilterEntityReferenceTest.php
@@ -0,0 +1,252 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\views_ui\FunctionalJavascript;
+
+use Drupal\Core\Url;
+use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+use Drupal\Tests\views_ui\Traits\FilterEntityReferenceTrait;
+
+/**
+ * Tests views creation wizard.
+ *
+ * @group views_ui
+ * @see \Drupal\views\Plugin\views\filter\EntityReference
+ */
+class FilterEntityReferenceTest extends WebDriverTestBase {
+
+ use FilterEntityReferenceTrait;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected static $modules = [
+ 'node',
+ 'views',
+ 'views_ui',
+ 'views_test_entity_reference',
+ ];
+
+ /**
+ * {@inheritdoc}
+ */
+ protected $defaultTheme = 'stark';
+
+ /**
+ * Views used by this test.
+ *
+ * @var array
+ */
+ public static $testViews = ['test_entity_reference'];
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setUp(): void {
+ parent::setUp();
+
+ $admin_user = $this->drupalCreateUser([
+ 'administer views',
+ ]);
+ $this->drupalLogin($admin_user);
+
+ $this->setUpEntityTypes();
+ }
+
+ /**
+ * Tests end to end creation of a Content Entity Reference filter.
+ */
+ public function testAddEntityReferenceFieldWithDefaultSelectionHandler(): void {
+ $this->drupalGet('admin/structure/views/view/content');
+ $assert = $this->assertSession();
+ $page = $this->getSession()->getPage();
+
+ // Open the dialog.
+ $page->clickLink('views-add-filter');
+
+ // Wait for the popup to open and the search field to be available.
+ $assert->waitForField('override[controls][options_search]');
+
+ // Test that the both entity_reference and numeric options are visible.
+ $this->assertTrue($page->findField('name[node__field_test.field_test_target_id]')
+ ->isVisible());
+ $this->assertTrue($page->findField('name[node__field_test.field_test_target_id]')
+ ->isVisible());
+ $page->findField('name[node__field_test.field_test_target_id]')
+ ->click();
+ $this->assertTrue($page->find('css', 'button.button.button--primary.form-submit.ui-button')
+ ->isVisible());
+ $page->find('css', 'button.button.button--primary.form-submit.ui-button')
+ ->click();
+
+ // Wait for the selection handler to show up.
+ $assert->waitForField('options[sub_handler]');
+ $page->selectFieldOption('options[sub_handler]', 'default:node');
+
+ // Check that that default handler target bundles are available.
+ $this->assertTrue($page->findField('options[reference_default:node][target_bundles][article]')
+ ->isVisible());
+ $this->assertTrue($page->findField('options[reference_default:node][target_bundles][page]')
+ ->isVisible());
+ $this->assertTrue($page->findField('options[widget]')->isVisible());
+
+ // Ensure that disabled form elements from selection handler do not show up
+ // @see \Drupal\views\Plugin\views\filter\EntityReference method
+ // buildExtraOptionsForm.
+ $this->assertFalse($page->hasField('options[reference_default:node][target_bundles_update]'));
+ $this->assertFalse($page->hasField('options[reference_default:node][auto_create]'));
+ $this->assertFalse($page->hasField('options[reference_default:node][auto_create_bundle]'));
+
+ // Choose the default handler using the select widget with article type
+ // checked.
+ $page->checkField('options[reference_default:node][target_bundles][article]');
+ $page->selectFieldOption('options[widget]', 'select');
+ $this->assertSame($page->findField('options[widget]')
+ ->getValue(), 'select');
+ $page->find('xpath', "//*[contains(text(), 'Apply and continue')]")
+ ->press();
+
+ // Test the exposed filter options show up correctly.
+ // Test the exposed filter options show up correctly.
+ $assert->waitForField('options[expose_button][checkbox][checkbox]');
+ $page->findField('options[expose_button][checkbox][checkbox]')->click();
+ $this->assertTrue($page->hasCheckedField('options[expose_button][checkbox][checkbox]'));
+
+ // Check the exposed filters multiple option.
+ $assert->waitForField('options[expose][multiple]');
+ $page->findField('options[expose][multiple]')->click();
+ $this->assertTrue($page->hasCheckedField('options[expose][multiple]'));
+ $page->find('css', '.ui-dialog .ui-dialog-buttonpane')->pressButton('Apply');
+ $assert->waitForElementRemoved('css', '.ui-dialog');
+
+ // Wait for the Views Preview to show up with the new reference field.
+ $assert->waitForField('field_test_config_target_id[]');
+ $this->assertTrue($page->findField('field_test_target_id[]')
+ ->isVisible());
+ $this->assertTrue($page->find('css', 'select[name="field_test_target_id[]"]')
+ ->hasAttribute('multiple'));
+
+ // Opening the settings form and change the handler to use an Entity
+ // Reference view.
+ // @see views.view.test_entity_reference.yml
+ $base_url = Url::fromRoute('entity.view.collection')->toString();
+ $url = $base_url . '/nojs/handler-extra/content/page_1/filter/field_test_target_id';
+ $extra_settings_selector = 'a[href="' . $url . '"]';
+ $element = $this->assertSession()->waitForElementVisible('css', $extra_settings_selector);
+ $this->assertNotNull($element);
+ $element->click();
+ $assert->waitForField('options[sub_handler]');
+ $page->selectFieldOption('options[sub_handler]', 'views');
+ $page->selectFieldOption('options[reference_views][view][view_and_display]', 'test_entity_reference:entity_reference');
+ $page->find('xpath', "//*[contains(text(), 'Apply')]")
+ ->press();
+ $assert->assertWaitOnAjaxRequest();
+
+ // The Views Reference filter has a title Filter to a single result, so
+ // ensure only that result is available as an option.
+ $assert->waitForElementRemoved('css', '.ui-dialog');
+
+ $this->assertCount(1, $page->findAll('css', 'select[name="field_test_target_id[]"] option'));
+
+ // Change to an autocomplete filter.
+ // Opening the settings form and change the handler to use an Entity
+ // Reference view.
+ // @see views.view.test_entity_reference.yml
+ $page->find('css', $extra_settings_selector)
+ ->click();
+ $assert->waitForElementVisible('named', [
+ 'radio',
+ 'options[widget]',
+ ]);
+ $page->selectFieldOption('options[widget]', 'autocomplete');
+ $this->assertSame($page->findField('options[widget]')
+ ->getValue(), 'autocomplete');
+ $this->getSession()
+ ->getPage()
+ ->find('xpath', "//*[contains(text(), 'Apply')]")
+ ->press();
+
+ // Check that it is now an autocomplete.
+ $assert->waitForField('field_test_target_id');
+ $page = $this->getSession()->getPage();
+ $this->assertTrue($page->findField('field_test_target_id')
+ ->isVisible());
+ $this->assertTrue($page->find('css', 'input[name="field_test_target_id"]')
+ ->hasAttribute('data-autocomplete-path'));
+ }
+
+ /**
+ * Tests end to end creation of a Config Entity Reference filter.
+ */
+ public function testAddConfigEntityReferenceFieldWithDefaultSelectionHandler(): void {
+ $this->drupalGet('admin/structure/views/view/content');
+ $assert = $this->assertSession();
+ $page = $this->getSession()->getPage();
+
+ // Open the 'Add filter dialog'.
+ $page->clickLink('views-add-filter');
+
+ // Wait for the popup to open and the search field to be available.
+ $assert->waitForField('override[controls][group]');
+
+ // Test that the entity_reference option is visible.
+ $this->assertTrue($page->findField('name[node__field_test_config.field_test_config_target_id]')->isVisible());
+ $page->findField('name[node__field_test_config.field_test_config_target_id]')->click();
+ $submitButton = $page->find('css', 'button.button.button--primary.form-submit.ui-button');
+ $this->assertTrue($submitButton->isVisible());
+ $submitButton->click();
+
+ // Wait for the selection handler to show up.
+ $assert->waitForField('options[sub_handler]');
+
+ $page->selectFieldOption('options[sub_handler]', 'default:node_type');
+
+ // Choose the default handler using the select widget with article type
+ // checked.
+ $page->selectFieldOption('options[widget]', 'select');
+ $this->assertSame('select', $page->findField('options[widget]')->getValue());
+ $page->find('xpath', "//*[contains(text(), 'Apply and continue')]")->press();
+
+ // Test the exposed filter options show up correctly.
+ $assert->waitForField('options[expose_button][checkbox][checkbox]');
+ $page->findField('options[expose_button][checkbox][checkbox]')->click();
+ $this->assertTrue($page->hasCheckedField('options[expose_button][checkbox][checkbox]'));
+
+ // Check the exposed filters multiple option.
+ $assert->waitForField('options[expose][multiple]');
+ $page->findField('options[expose][multiple]')->click();
+ $this->assertTrue($page->hasCheckedField('options[expose][multiple]'));
+ $page->find('css', '.ui-dialog .ui-dialog-buttonpane')->pressButton('Apply');
+ $assert->waitForElementRemoved('css', '.ui-dialog');
+
+ // Wait for the Views Preview to show up with the reference field.
+ $assert->waitForField('field_test_config_target_id[]');
+ $this->assertTrue($page->findField('field_test_config_target_id[]')->isVisible());
+ $this->assertTrue($page->find('css', 'select[name="field_test_config_target_id[]"]')->hasAttribute('multiple'));
+
+ // Check references config options.
+ $options = $page->findAll('css', 'select[name="field_test_config_target_id[]"] option');
+ $this->assertCount(2, $options);
+ $this->assertSame('article', $options[0]->getValue());
+ $this->assertSame('page', $options[1]->getValue());
+
+ $base_url = Url::fromRoute('entity.view.collection')->toString();
+ $url = $base_url . '/nojs/handler-extra/content/page_1/filter/field_test_config_target_id';
+ $extra_settings_selector = 'a[href="' . $url . '"]';
+
+ // Change to an autocomplete filter.
+ $page->find('css', $extra_settings_selector)->click();
+ $assert->waitForField('options[widget]');
+ $page->selectFieldOption('options[widget]', 'autocomplete');
+ $this->assertSame('autocomplete', $page->findField('options[widget]')->getValue());
+ $page->find('css', '.ui-dialog .ui-dialog-buttonpane')->pressButton('Apply');
+ $this->assertSession()->assertWaitOnAjaxRequest();
+
+ // Check that it is now an autocomplete input.
+ $assert->waitForField('field_test_config_target_id');
+ $this->assertTrue($page->findField('field_test_config_target_id')->isVisible());
+ $this->assertTrue($page->find('css', 'input[name="field_test_config_target_id"]')->hasAttribute('data-autocomplete-path'));
+ }
+
+}
diff --git a/core/modules/views_ui/tests/src/Traits/FilterEntityReferenceTrait.php b/core/modules/views_ui/tests/src/Traits/FilterEntityReferenceTrait.php
new file mode 100644
index 0000000000000000000000000000000000000000..83e58fbaf49b5b8a92ee0c95447ff1ac17e63eae
--- /dev/null
+++ b/core/modules/views_ui/tests/src/Traits/FilterEntityReferenceTrait.php
@@ -0,0 +1,135 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\views_ui\Traits;
+
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\node\NodeTypeInterface;
+use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
+use Drupal\Tests\node\Traits\NodeCreationTrait;
+
+/**
+ * Sets up the entity types and relationships for entity reference tests.
+ *
+ * This trait is meant to be used only by test classes.
+ */
+trait FilterEntityReferenceTrait {
+
+ use ContentTypeCreationTrait {
+ createContentType as drupalCreateContentType;
+ }
+ use NodeCreationTrait {
+ getNodeByTitle as drupalGetNodeByTitle;
+ createNode as drupalCreateNode;
+ }
+
+ /**
+ * The host content type to add the entity reference field to.
+ *
+ * @var \Drupal\node\NodeTypeInterface
+ */
+ protected NodeTypeInterface $hostBundle;
+
+ /**
+ * The content type to be referenced by the host content type.
+ *
+ * @var \Drupal\node\NodeTypeInterface
+ */
+ protected NodeTypeInterface $targetBundle;
+
+ /**
+ * Entities to be used as reference targets.
+ *
+ * @var \Drupal\node\NodeInterface[]
+ */
+ protected array $targetEntities;
+
+ /**
+ * Host entities which contain the reference fields to the target entities.
+ *
+ * @var \Drupal\node\NodeInterface[]
+ */
+ protected array $hostEntities;
+
+ /**
+ * Sets up the entity types and relationships.
+ */
+ protected function setUpEntityTypes(): void {
+ // Create an entity type, and a referenceable type. Since these are coded
+ // into the test view, they are not randomly named.
+ $this->hostBundle = $this->drupalCreateContentType(['type' => 'page']);
+ $this->targetBundle = $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->hostBundle->id(),
+ 'settings' => [
+ 'handler' => 'default',
+ 'handler_settings' => [
+ 'target_bundles' => [
+ $this->targetBundle->id() => $this->targetBundle->label(),
+ ],
+ ],
+ ],
+ ]);
+ $field->save();
+
+ // Create 10 nodes for use as target entities.
+ for ($i = 0; $i < 10; $i++) {
+ $node = $this->drupalCreateNode([
+ 'type' => $this->targetBundle->id(),
+ 'title' => ucfirst($this->targetBundle->id()) . ' ' . $i,
+ ]);
+ $this->targetEntities[$node->id()] = $node;
+ }
+
+ // Create 1 host entity to reference target entities from.
+ $node = $this->drupalCreateNode([
+ 'type' => $this->hostBundle->id(),
+ 'title' => ucfirst($this->hostBundle->id()) . ' 0',
+ ]);
+ $this->hostEntities = [
+ $node->id() => $node,
+ ];
+
+ $field_storage = FieldStorageConfig::create([
+ 'entity_type' => 'node',
+ 'field_name' => 'field_test_config',
+ 'type' => 'entity_reference',
+ 'settings' => [
+ 'target_type' => 'node_type',
+ ],
+ 'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
+ ]);
+ $field_storage->save();
+
+ $field = FieldConfig::create([
+ 'entity_type' => 'node',
+ 'field_name' => 'field_test_config',
+ 'bundle' => $this->hostBundle->id(),
+ 'settings' => [
+ 'handler' => 'default',
+ 'handler_settings' => [
+ 'sort' => ['field' => '_none'],
+ ],
+ ],
+ ]);
+ $field->save();
+ }
+
+}