diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml index 41332e3..94bfd60 100644 --- a/core/config/schema/core.data_types.schema.yml +++ b/core/config/schema/core.data_types.schema.yml @@ -557,6 +557,15 @@ field.storage_settings.entity_reference: target_type: type: string label: 'Type of item to reference' + target_bundle: + type: string + label: 'Bundle of item to reference' + additional_behaviors: + type: sequence + label: 'Additional behaviors' + sequence: + - type: boolean + label: 'Behavior' field.field_settings.entity_reference: type: mapping diff --git a/core/modules/entity_reference/config/schema/entity_reference.views.schema.yml b/core/modules/entity_reference/config/schema/entity_reference.views.schema.yml index f92af7b..8597bbb 100644 --- a/core/modules/entity_reference/config/schema/entity_reference.views.schema.yml +++ b/core/modules/entity_reference/config/schema/entity_reference.views.schema.yml @@ -18,3 +18,24 @@ views.style.entity_reference: sequence: - type: string label: 'Search field' + +views.filter.entity_reference: + type: views.filter.many_to_one + label: 'Entity reference' + mapping: + target_bundles: + type: sequence + label: 'Content types' + sequence: + - type: string + label: 'Content type' + sort: + type: mapping + label: 'Sorting' + mapping: + field: + type: string + label: 'Field' + direction: + type: string + label: 'Direction' diff --git a/core/modules/entity_reference/src/ConfigurableEntityReferenceItem.php b/core/modules/entity_reference/src/ConfigurableEntityReferenceItem.php index 668554d..f321c3a 100644 --- a/core/modules/entity_reference/src/ConfigurableEntityReferenceItem.php +++ b/core/modules/entity_reference/src/ConfigurableEntityReferenceItem.php @@ -39,9 +39,8 @@ public static function defaultStorageSettings() { // The target bundle is handled by the 'target_bundles' property in the // 'handler_settings' instance setting. unset($settings['target_bundle']); - $settings['additional_behaviors'] = array( - 'views_select_list' => TRUE, + 'views_select_list' => FALSE, ); return $settings; diff --git a/core/modules/entity_reference/src/Plugin/views/filter/EntityReference.php b/core/modules/entity_reference/src/Plugin/views/filter/EntityReference.php index 803c0fa..1b3e294 100644 --- a/core/modules/entity_reference/src/Plugin/views/filter/EntityReference.php +++ b/core/modules/entity_reference/src/Plugin/views/filter/EntityReference.php @@ -8,10 +8,11 @@ namespace Drupal\entity_reference\Plugin\views\filter; use Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface; -use Drupal\Core\Field\FieldDefinitionInterface; -use Drupal\field\Entity\FieldConfig; -use Drupal\field\Entity\FieldStorageConfig; +use Drupal\Core\Form\FormStateInterface; +use Drupal\field\Views\FieldAPIHandlerTrait; +use Drupal\views\Plugin\views\display\DisplayPluginBase; use Drupal\views\Plugin\views\filter\ManyToOne; +use Drupal\views\ViewExecutable; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -23,10 +24,7 @@ */ class EntityReference extends ManyToOne { - /** - * @var \Drupal\Core\Field\FieldDefinitionInterface - */ - protected $fieldDefinition; + use FieldAPIHandlerTrait; /** * @var \Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface @@ -45,38 +43,92 @@ class EntityReference extends ManyToOne { * @param \Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManagerInterface $selection_plugin_manager * Entity reference selection plugin manager */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, SelectionPluginManagerInterface $selection_plugin_manager, FieldDefinitionInterface $field_definition) { + public function __construct(array $configuration, $plugin_id, $plugin_definition, SelectionPluginManagerInterface $selection_plugin_manager) { parent::__construct($configuration, $plugin_id, $plugin_definition); - $this->fieldDefinition = $field_definition; - $this->selectionPluginManager = $selection_plugin_manager; + $this->selectionPluginManager = $selection_plugin_manager; + } + + /** + * {@inheritdoc} + */ + public function buildExtraOptionsForm(&$form, FormStateInterface $form_state) { + // Replicate field config form since these settings don't reside in field + // storage. + $handler = $this->selectionPluginManager->getInstance($this->getSelectionHandlerOptions()); + $form += $handler->buildConfigurationForm($form, $form_state); } /** * {@inheritdoc} */ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { - // @todo How to get a FieldDefinitionInterface (required by - // getSelectionHandler) without a bundle? `$configuration` contains - // `entity_type` and `field_name` parameters. - $field_definition = NULL; return new static( $configuration, $plugin_id, $plugin_definition, - $container->get('plugin.manager.entity_reference_selection'), - $field_definition + $container->get('plugin.manager.entity_reference_selection') ); } /** + * {@inheritdoc} + */ + protected function defineOptions() { + $options = parent::defineOptions(); + + $options['target_bundles'] = array('default' => array()); + $options['sort'] = array('default' => array()); + + return $options; + } + + /** * Provide a list of all values for this entity reference field. * * {@inheritdoc} */ public function getValueOptions() { - // @todo use `getReferenceableEntities()` if possible. See above @todo. - // $entities = $this->selectionPluginManager->getSelectionHandler($this->fieldDefinition)->getReferenceableEntities(); - // If not possible, a direct entity query as done in SelectionBase::getReferenceableEntities(). + $entities = $this->selectionPluginManager->getInstance($this->getSelectionHandlerOptions())->getReferenceableEntities(); + $return = []; + foreach ($this->options['target_bundles'] as $bundle) { + if (isset($entities[$bundle])) { + $return += $entities[$bundle]; + } + } + + $this->valueOptions = $return; + return $return; + } + + /** + * {@inheritdoc} + */ + public function hasExtraOptions() { + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) { + parent::init($view, $display, $options); + + if (!empty($this->definition['target_bundles'])) { + $this->options['target_bundles'] = $this->definition['target_bundles']; + } + } + + /** + * Helper method to get selection handler options. + * + * @return array + * Array of options matching SelectionBase options. + */ + protected function getSelectionHandlerOptions() { + $options = ['target_type' => $this->configuration['entity_type']]; + $options['handler_settings']['target_bundles'] = $this->options['target_bundles']; + $options['handler_settings']['sort'] = $this->options['sort']; + return $options; } } diff --git a/core/modules/entity_reference/src/Tests/Views/ViewsFilterUiTest.php b/core/modules/entity_reference/src/Tests/Views/ViewsFilterUiTest.php new file mode 100644 index 0000000..aca4825 --- /dev/null +++ b/core/modules/entity_reference/src/Tests/Views/ViewsFilterUiTest.php @@ -0,0 +1,166 @@ +entityType = $this->drupalCreateContentType(['type' => 'page']); + $this->referencableType = $this->drupalCreateContentType(['type' => 'article']); + + $field_storage = FieldStorageConfig::create(array( + 'entity_type' => 'node', + 'field_name' => 'field_test', + 'type' => 'entity_reference', + 'settings' => array( + 'target_type' => 'node', + 'additional_behaviors' => array( + 'views_select_list' => TRUE, + ), + ), + 'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, + )); + $field_storage->save(); + + $field = FieldConfig::create(array( + 'entity_type' => 'node', + 'field_name' => 'field_test', + 'bundle' => $this->entityType->id(), + 'settings' => array( + 'handler' => 'default', + 'handler_settings' => array( + // Note, this has no impact on Views at this time. + 'target_bundles' => array( + $this->referencableType->id() => $this->referencableType->label(), + ), + ), + ), + )); + $field->save(); + + ViewTestData::createTestViews(get_class($this), array('entity_reference_test_views')); + + // Create 10 referencable nodes. + for ($i = 0; $i < 10; $i++) { + $node = $this->drupalCreateNode(['type' => $this->referencableType->id()]); + $this->referencableNodes[$node->id()] = $node; + } + } + + /** + * Tests the filter UI. + */ + public function testFilterUi() { + $this->drupalGet('admin/structure/views/nojs/handler/test_entity_reference_node_view/default/filter/field_test_target_id'); + + $options = $this->getUiOptions(); + // Should be sorted by title ASC. + uasort($this->referencableNodes, function (NodeInterface $a, NodeInterface $b) { + return strnatcasecmp($a->getTitle(), $b->getTitle()); + }); + $found_all = TRUE; + $i = 0; + foreach ($this->referencableNodes as $nid => $node) { + $option = $options[$i]; + $label = $option['label']; + $found_all = $found_all && $label == $node->label() && $nid == $option['nid']; + $this->assertEqual($label, $node->label(), String::format('Expected referencable label found for option !option', ['!option' => $i])); + $i++; + } + $this->assertTrue($found_all, 'All referencable nodes were available as a select list properly ordered.'); + + // Change the sort field and direction. + $view = View::load('test_entity_reference_node_view'); + $display =& $view->getDisplay('default'); + $display['display_options']['filters']['field_test_target_id']['sort']['field'] = 'nid'; + $display['display_options']['filters']['field_test_target_id']['sort']['direction'] = 'DESC'; + $view->save(); + + $this->drupalGet('admin/structure/views/nojs/handler/test_entity_reference_node_view/default/filter/field_test_target_id'); + // Items should now be in reverse nid order. + krsort($this->referencableNodes); + $options = $this->getUiOptions(); + $found_all = TRUE; + $i = 0; + foreach ($this->referencableNodes as $nid => $node) { + $option = $options[$i]; + $label = $option['label']; + $found_all = $found_all && $label == $node->label() && $nid == $option['nid']; + $this->assertEqual($label, $node->label(), String::format('Expected referencable label found for option !option', ['!option' => $i])); + $i++; + } + $this->assertTrue($found_all, 'All referencable nodes were available as a select list properly ordered.'); + } + + /** + * 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 SimpleXMLElement[] $result */ + $result = $this->xpath('//select[@id="edit-options-value"]/option'); + $first = array_shift($result); + $this->assertEqual($first->attributes()->value, 'all', 'First option is properly set to "all".'); + + $options = []; + foreach ($result as $option) { + $nid = (int) $option->attributes()['value']; + $options[] = ['nid' => $nid, 'label' => (string) $option]; + } + + return $options; + } + +} diff --git a/core/modules/entity_reference/tests/modules/entity_reference_test_views/test_views/views.view.test_entity_reference_node_view.yml b/core/modules/entity_reference/tests/modules/entity_reference_test_views/test_views/views.view.test_entity_reference_node_view.yml new file mode 100644 index 0000000..0cb0d1a --- /dev/null +++ b/core/modules/entity_reference/tests/modules/entity_reference_test_views/test_views/views.view.test_entity_reference_node_view.yml @@ -0,0 +1,207 @@ +langcode: en +status: true +dependencies: + config: + - node.type.page + module: + - entity_reference + - node + - user +id: test_entity_reference_node_view +label: Test reference filters +module: views +description: '' +tag: '' +base_table: node +base_field: nid +core: 8.x +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: full + 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: '‹ previous' + next: 'next ›' + first: '« first' + last: 'last »' + quantity: 9 + 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 + link_to_node: true + plugin_id: node + 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 + filters: + status: + value: true + 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: 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: '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: 0 + 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: 0 + target_bundles: + article: article + sort: + field: title + direction: ASC + 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: { }