From 28dac554730629036261aecbfb456c427ed8fb58 Mon Sep 17 00:00:00 2001 From: catch <catch@35733.no-reply.drupal.org> Date: Fri, 31 May 2024 09:23:04 +0100 Subject: [PATCH] Issue #2640994 by dww, tduong, Alex Bukach, Berdir, ameymudras, catch, mohit_aghera, raman.b, jofitz, Ben Buske, ankithashetty, smustgrave, nikitagupta, nicxvan, FeyP, pameeela, technoveltyco, dawehner, kim.pepper, Lendude: Fix label token replacement for views entity reference arguments --- .../file/src/Plugin/views/argument/Fid.php | 62 +---- .../node/src/Plugin/views/argument/Nid.php | 57 +---- .../src/Plugin/views/argument/Taxonomy.php | 43 +--- .../Plugin/views/argument/VocabularyVid.php | 56 +---- .../argument/TaxonomyViewsArgumentTest.php | 41 ---- .../user/src/Plugin/views/argument/Uid.php | 56 +---- .../config/schema/views.argument.schema.yml | 12 + core/modules/views/src/EntityViewsData.php | 5 +- .../Plugin/views/argument/EntityArgument.php | 63 +++++ .../argument/EntityReferenceArgument.php | 64 +++++ core/modules/views/src/ViewsConfigUpdater.php | 71 +++++- .../fixtures/update/entity-id-argument.php | 21 ++ ...ws.view.test_entity_id_argument_update.yml | 228 ++++++++++++++++++ ...iew.test_argument_default_current_user.yml | 2 +- .../views.view.test_argument_dependency.yml | 2 +- .../views.view.test_entity_id_argument.yml | 227 +++++++++++++++++ .../Functional/Plugin/EntityArgumentTest.php | 85 +++++++ .../Update/EntityArgumentUpdateTest.php | 47 ++++ .../src/Kernel/Entity/EntityViewsDataTest.php | 3 +- .../tests/src/Kernel/ViewExecutableTest.php | 11 +- core/modules/views/views.post_update.php | 16 ++ core/modules/views/views.views.inc | 10 +- .../install/views.view.related_recipes.yml | 3 +- 23 files changed, 869 insertions(+), 316 deletions(-) delete mode 100644 core/modules/taxonomy/tests/modules/taxonomy_test/src/Plugin/views/argument/TaxonomyViewsArgumentTest.php create mode 100644 core/modules/views/src/Plugin/views/argument/EntityArgument.php create mode 100644 core/modules/views/src/Plugin/views/argument/EntityReferenceArgument.php create mode 100644 core/modules/views/tests/fixtures/update/entity-id-argument.php create mode 100644 core/modules/views/tests/fixtures/update/views.view.test_entity_id_argument_update.yml create mode 100644 core/modules/views/tests/modules/views_test_config/test_views/views.view.test_entity_id_argument.yml create mode 100644 core/modules/views/tests/src/Functional/Plugin/EntityArgumentTest.php create mode 100644 core/modules/views/tests/src/Functional/Update/EntityArgumentUpdateTest.php diff --git a/core/modules/file/src/Plugin/views/argument/Fid.php b/core/modules/file/src/Plugin/views/argument/Fid.php index b37a3feefa07..4eb1698c9d8b 100644 --- a/core/modules/file/src/Plugin/views/argument/Fid.php +++ b/core/modules/file/src/Plugin/views/argument/Fid.php @@ -2,11 +2,8 @@ namespace Drupal\file\Plugin\views\argument; -use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\views\Attribute\ViewsArgument; -use Drupal\views\Plugin\views\argument\NumericArgument; -use Drupal\Core\Plugin\ContainerFactoryPluginInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\views\Plugin\views\argument\EntityArgument; /** * Argument handler to accept multiple file ids. @@ -16,59 +13,4 @@ #[ViewsArgument( id: 'file_fid', )] -class Fid extends NumericArgument implements ContainerFactoryPluginInterface { - - /** - * The entity type manager. - * - * @var \Drupal\Core\Entity\EntityTypeManagerInterface - */ - protected $entityTypeManager; - - /** - * Constructs a Drupal\file\Plugin\views\argument\Fid object. - * - * @param array $configuration - * A configuration array containing information about the plugin instance. - * @param string $plugin_id - * The plugin_id for the plugin instance. - * @param mixed $plugin_definition - * The plugin implementation definition. - * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager - * The entity type manager. - */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager) { - parent::__construct($configuration, $plugin_id, $plugin_definition); - $this->entityTypeManager = $entity_type_manager; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { - return new static( - $configuration, - $plugin_id, - $plugin_definition, - $container->get('entity_type.manager') - ); - } - - /** - * Override the behavior of titleQuery(). Get the filenames. - */ - public function titleQuery() { - $storage = $this->entityTypeManager->getStorage('file'); - $fids = $storage->getQuery() - ->accessCheck(FALSE) - ->condition('fid', $this->value, 'IN') - ->execute(); - $files = $storage->loadMultiple($fids); - $titles = []; - foreach ($files as $file) { - $titles[] = $file->getFilename(); - } - return $titles; - } - -} +class Fid extends EntityArgument {} diff --git a/core/modules/node/src/Plugin/views/argument/Nid.php b/core/modules/node/src/Plugin/views/argument/Nid.php index 4f499e7c874a..0329a4725b4a 100644 --- a/core/modules/node/src/Plugin/views/argument/Nid.php +++ b/core/modules/node/src/Plugin/views/argument/Nid.php @@ -2,10 +2,8 @@ namespace Drupal\node\Plugin\views\argument; -use Drupal\node\NodeStorageInterface; use Drupal\views\Attribute\ViewsArgument; -use Drupal\views\Plugin\views\argument\NumericArgument; -use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\views\Plugin\views\argument\EntityArgument; /** * Argument handler to accept a node id. @@ -13,55 +11,4 @@ #[ViewsArgument( id: 'node_nid', )] -class Nid extends NumericArgument { - - /** - * The node storage. - * - * @var \Drupal\node\NodeStorageInterface - */ - protected $nodeStorage; - - /** - * Constructs the Nid object. - * - * @param array $configuration - * A configuration array containing information about the plugin instance. - * @param string $plugin_id - * The plugin_id for the plugin instance. - * @param mixed $plugin_definition - * The plugin implementation definition. - * @param \Drupal\node\NodeStorageInterface $node_storage - * The node storage handler. - */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, NodeStorageInterface $node_storage) { - parent::__construct($configuration, $plugin_id, $plugin_definition); - $this->nodeStorage = $node_storage; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { - return new static( - $configuration, - $plugin_id, - $plugin_definition, - $container->get('entity_type.manager')->getStorage('node') - ); - } - - /** - * Override the behavior of title(). Get the title of the node. - */ - public function titleQuery() { - $titles = []; - - $nodes = $this->nodeStorage->loadMultiple($this->value); - foreach ($nodes as $node) { - $titles[] = $node->label(); - } - return $titles; - } - -} +class Nid extends EntityArgument {} diff --git a/core/modules/taxonomy/src/Plugin/views/argument/Taxonomy.php b/core/modules/taxonomy/src/Plugin/views/argument/Taxonomy.php index dcdd2e5cfa37..efc8aceed671 100644 --- a/core/modules/taxonomy/src/Plugin/views/argument/Taxonomy.php +++ b/core/modules/taxonomy/src/Plugin/views/argument/Taxonomy.php @@ -2,11 +2,8 @@ namespace Drupal\taxonomy\Plugin\views\argument; -use Drupal\Core\Entity\EntityRepositoryInterface; -use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\views\Attribute\ViewsArgument; -use Drupal\views\Plugin\views\argument\NumericArgument; -use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\views\Plugin\views\argument\EntityArgument; /** * Argument handler for basic taxonomy tid. @@ -16,40 +13,4 @@ #[ViewsArgument( id: 'taxonomy', )] -class Taxonomy extends NumericArgument implements ContainerFactoryPluginInterface { - - /** - * {@inheritdoc} - */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, protected EntityRepositoryInterface $entityRepository) { - parent::__construct($configuration, $plugin_id, $plugin_definition); - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { - return new static( - $configuration, - $plugin_id, - $plugin_definition, - $container->get('entity.repository') - ); - } - - /** - * Override the behavior of title(). Get the title of the node. - */ - public function title() { - // There might be no valid argument. - if ($this->argument) { - $term = $this->entityRepository->getCanonical('taxonomy_term', $this->argument); - if (!empty($term)) { - return $term->label(); - } - } - // TODO review text - return $this->t('No name'); - } - -} +class Taxonomy extends EntityArgument {} diff --git a/core/modules/taxonomy/src/Plugin/views/argument/VocabularyVid.php b/core/modules/taxonomy/src/Plugin/views/argument/VocabularyVid.php index 2bc9ab387c1f..2384daf76011 100644 --- a/core/modules/taxonomy/src/Plugin/views/argument/VocabularyVid.php +++ b/core/modules/taxonomy/src/Plugin/views/argument/VocabularyVid.php @@ -3,9 +3,7 @@ namespace Drupal\taxonomy\Plugin\views\argument; use Drupal\views\Attribute\ViewsArgument; -use Drupal\views\Plugin\views\argument\NumericArgument; -use Drupal\taxonomy\VocabularyStorageInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\views\Plugin\views\argument\EntityArgument; /** * Argument handler to accept a vocabulary id. @@ -15,54 +13,4 @@ #[ViewsArgument( id: 'vocabulary_vid', )] -class VocabularyVid extends NumericArgument { - - /** - * The vocabulary storage. - * - * @var \Drupal\taxonomy\VocabularyStorageInterface - */ - protected $vocabularyStorage; - - /** - * Constructs the VocabularyVid object. - * - * @param array $configuration - * A configuration array containing information about the plugin instance. - * @param string $plugin_id - * The plugin_id for the plugin instance. - * @param mixed $plugin_definition - * The plugin implementation definition. - * @param \Drupal\taxonomy\VocabularyStorageInterface $vocabulary_storage - * The vocabulary storage. - */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, VocabularyStorageInterface $vocabulary_storage) { - parent::__construct($configuration, $plugin_id, $plugin_definition); - $this->vocabularyStorage = $vocabulary_storage; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { - return new static( - $configuration, - $plugin_id, - $plugin_definition, - $container->get('entity_type.manager')->getStorage('taxonomy_vocabulary') - ); - } - - /** - * Override the behavior of title(). Get the name of the vocabulary. - */ - public function title() { - $vocabulary = $this->vocabularyStorage->load($this->argument); - if ($vocabulary) { - return $vocabulary->label(); - } - - return $this->t('No vocabulary'); - } - -} +class VocabularyVid extends EntityArgument {} diff --git a/core/modules/taxonomy/tests/modules/taxonomy_test/src/Plugin/views/argument/TaxonomyViewsArgumentTest.php b/core/modules/taxonomy/tests/modules/taxonomy_test/src/Plugin/views/argument/TaxonomyViewsArgumentTest.php deleted file mode 100644 index ec14d55f1a0d..000000000000 --- a/core/modules/taxonomy/tests/modules/taxonomy_test/src/Plugin/views/argument/TaxonomyViewsArgumentTest.php +++ /dev/null @@ -1,41 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace Drupal\taxonomy_test\Plugin\views\argument; - -use Drupal\Core\Entity\EntityStorageInterface; -use Drupal\Core\Entity\EntityTypeBundleInfoInterface; -use Drupal\taxonomy\Plugin\views\argument\Taxonomy; -use Symfony\Component\DependencyInjection\ContainerInterface; - -/** - * Test argument handler for testing deprecation in IndexTidDepth plugin. - * - * @ingroup views_argument_handlers - * - * @ViewsArgument("taxonomy_views_argument_test") - */ -class TaxonomyViewsArgumentTest extends Taxonomy { - - /** - * Constructs new IndexTidDepthTestPlugin object. - */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityStorageInterface $term_storage, protected EntityTypeBundleInfoInterface $entityTypeBundleInfo) { - parent::__construct($configuration, $plugin_id, $plugin_definition, $term_storage); - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static { - return new static( - $configuration, - $plugin_id, - $plugin_definition, - $container->get('entity_type.manager')->getStorage('taxonomy_term'), - $container->get('entity_type.bundle.info'), - ); - } - -} diff --git a/core/modules/user/src/Plugin/views/argument/Uid.php b/core/modules/user/src/Plugin/views/argument/Uid.php index 3de3a18a43fc..806db2643e57 100644 --- a/core/modules/user/src/Plugin/views/argument/Uid.php +++ b/core/modules/user/src/Plugin/views/argument/Uid.php @@ -2,10 +2,8 @@ namespace Drupal\user\Plugin\views\argument; -use Drupal\Core\Entity\EntityStorageInterface; use Drupal\views\Attribute\ViewsArgument; -use Drupal\views\Plugin\views\argument\NumericArgument; -use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\views\Plugin\views\argument\EntityArgument; /** * Argument handler to accept a user id. @@ -15,54 +13,4 @@ #[ViewsArgument( id: 'user_uid' )] -class Uid extends NumericArgument { - - /** - * The user storage. - * - * @var \Drupal\Core\Entity\EntityStorageInterface - */ - protected $storage; - - /** - * Constructs a \Drupal\user\Plugin\views\argument\Uid object. - * - * @param array $configuration - * A configuration array containing information about the plugin instance. - * @param string $plugin_id - * The plugin_id for the plugin instance. - * @param mixed $plugin_definition - * The plugin implementation definition. - * @param \Drupal\Core\Entity\EntityStorageInterface $storage - * The user storage. - */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityStorageInterface $storage) { - parent::__construct($configuration, $plugin_id, $plugin_definition); - $this->storage = $storage; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { - return new static( - $configuration, - $plugin_id, - $plugin_definition, - $container->get('entity_type.manager')->getStorage('user') - ); - } - - /** - * Override the behavior of title(). Get the name of the user. - * - * @return array - * A list of usernames. - */ - public function titleQuery() { - return array_map(function ($account) { - return $account->label(); - }, $this->storage->loadMultiple($this->value)); - } - -} +class Uid extends EntityArgument {} diff --git a/core/modules/views/config/schema/views.argument.schema.yml b/core/modules/views/config/schema/views.argument.schema.yml index 2f43282809b7..5c1e3afae68d 100644 --- a/core/modules/views/config/schema/views.argument.schema.yml +++ b/core/modules/views/config/schema/views.argument.schema.yml @@ -40,6 +40,18 @@ views.argument.numeric: type: boolean label: 'Exclude' +views.argument.entity_id: + type: views.argument.numeric + label: 'Entity ID' + +views.argument.entity_target_id: + type: views.argument.numeric + label: 'Entity Target ID' + mapping: + target_entity_type_id: + type: string + label: 'Target entity type ID' + views.argument.string: type: views_argument label: 'String' diff --git a/core/modules/views/src/EntityViewsData.php b/core/modules/views/src/EntityViewsData.php index 8acb1ad2979f..0550fb213eb6 100644 --- a/core/modules/views/src/EntityViewsData.php +++ b/core/modules/views/src/EntityViewsData.php @@ -644,7 +644,10 @@ protected function processViewsDataForEntityReference($table, FieldDefinitionInt 'id' => 'standard', ]; $views_field['field']['id'] = 'field'; - $views_field['argument']['id'] = 'numeric'; + // Provide an argument plugin that has a meaningful titleQuery() + // implementation getting the entity label. + $views_field['argument']['id'] = 'entity_target_id'; + $views_field['argument']['target_entity_type_id'] = $entity_type_id; $views_field['filter']['id'] = 'numeric'; $views_field['sort']['id'] = 'standard'; } diff --git a/core/modules/views/src/Plugin/views/argument/EntityArgument.php b/core/modules/views/src/Plugin/views/argument/EntityArgument.php new file mode 100644 index 000000000000..f25c9565fba2 --- /dev/null +++ b/core/modules/views/src/Plugin/views/argument/EntityArgument.php @@ -0,0 +1,63 @@ +<?php + +namespace Drupal\views\Plugin\views\argument; + +use Drupal\Core\Entity\EntityRepositoryInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\views\Attribute\ViewsArgument; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Argument handler to accept an entity ID value. + * + * This handler accepts the identifiers of entities themselves. The definition + * defines the `entity_type` parameter to determine what kind of ID to load. + * Entity reference ID values are handled by EntityReferenceArgument. + * + * @see \Drupal\views\Plugin\views\argument\EntityReferenceArgument + * + * @ingroup views_argument_handlers + */ +#[ViewsArgument( + id: 'entity_id', +)] +class EntityArgument extends NumericArgument implements ContainerFactoryPluginInterface { + + public function __construct( + array $configuration, + $plugin_id, + $plugin_definition, + protected EntityRepositoryInterface $entityRepository, + protected EntityTypeManagerInterface $entityTypeManager, + ) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity.repository'), + $container->get('entity_type.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function titleQuery() { + $titles = []; + + $entities = $this->entityTypeManager->getStorage($this->definition['entity_type'])->loadMultiple($this->value); + foreach ($entities as $entity) { + $titles[$entity->id()] = $this->entityRepository->getTranslationFromContext($entity)->label(); + } + return $titles; + } + +} diff --git a/core/modules/views/src/Plugin/views/argument/EntityReferenceArgument.php b/core/modules/views/src/Plugin/views/argument/EntityReferenceArgument.php new file mode 100644 index 000000000000..fecb31ee7d23 --- /dev/null +++ b/core/modules/views/src/Plugin/views/argument/EntityReferenceArgument.php @@ -0,0 +1,64 @@ +<?php + +namespace Drupal\views\Plugin\views\argument; + +use Drupal\Core\Entity\EntityRepositoryInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\views\Attribute\ViewsArgument; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Argument handler to accept an entity reference ID value. + * + * This handler accepts entity reference ID values. The definition defines the + * `target_entity_type_id` parameter to determine what kind of ID to load. + * Entity ID values that are directly part of an entity are handled by + * EntityArgument. + * + * @see \Drupal\views\Plugin\views\argument\EntityArgument + * + * @ingroup views_argument_handlers + */ +#[ViewsArgument( + id: 'entity_target_id' +)] +class EntityReferenceArgument extends NumericArgument implements ContainerFactoryPluginInterface { + + public function __construct( + array $configuration, + $plugin_id, + $plugin_definition, + protected EntityRepositoryInterface $entityRepository, + protected EntityTypeManagerInterface $entityTypeManager, + ) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity.repository'), + $container->get('entity_type.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function titleQuery() { + $titles = []; + + $entities = $this->entityTypeManager->getStorage($this->definition['target_entity_type_id'])->loadMultiple($this->value); + foreach ($entities as $entity) { + $titles[$entity->id()] = $this->entityRepository->getTranslationFromContext($entity)->label(); + } + return $titles; + } + +} diff --git a/core/modules/views/src/ViewsConfigUpdater.php b/core/modules/views/src/ViewsConfigUpdater.php index da4280100c5d..bb0d5c73a362 100644 --- a/core/modules/views/src/ViewsConfigUpdater.php +++ b/core/modules/views/src/ViewsConfigUpdater.php @@ -126,8 +126,11 @@ public function setDeprecationsEnabled($enabled) { * Whether the view was updated. */ public function updateAll(ViewEntityInterface $view) { - return $this->processDisplayHandlers($view, FALSE, function (&$handler, $handler_type, $key, $display_id) { + return $this->processDisplayHandlers($view, FALSE, function (&$handler, $handler_type, $key, $display_id) use ($view) { $changed = FALSE; + if ($this->processEntityArgumentUpdate($view)) { + $changed = TRUE; + } return $changed; }); } @@ -192,4 +195,70 @@ protected function processDisplayHandlers(ViewEntityInterface $view, $return_on_ return $changed; } + /** + * Checks if 'numeric' arguments should be converted to 'entity_target_id'. + * + * @param \Drupal\views\ViewEntityInterface $view + * The view entity. + * + * @return bool + * TRUE if the view has any arguments that reference an entity reference + * that need to be converted from 'numeric' to 'entity_target_id'. + */ + public function needsEntityArgumentUpdate(ViewEntityInterface $view): bool { + return $this->processDisplayHandlers($view, TRUE, function (&$handler, $handler_type) use ($view) { + return $this->processEntityArgumentUpdate($view); + }); + } + + /** + * Processes arguments and convert 'numeric' to 'entity_target_id' if needed. + * + * Note that since this update will trigger deprecations if called by + * views_view_presave(), we cannot rely on the usual handler-specific checking + * and processing. That would still hit views_view_presave(), even when + * invoked from post_update. We must directly update the view here, so that + * it's already correct by the time views_view_presave() sees it. + * + * @param \Drupal\views\ViewEntityInterface $view + * The View being updated. + * + * @return bool + * Whether the view was updated. + */ + public function processEntityArgumentUpdate(ViewEntityInterface $view): bool { + $changed = FALSE; + + $displays = $view->get('display'); + foreach ($displays as &$display) { + if (isset($display['display_options']['arguments'])) { + foreach ($display['display_options']['arguments'] as $argument_id => $argument) { + $plugin_id = $argument['plugin_id'] ?? ''; + if ($plugin_id === 'numeric') { + $argument_table_data = $this->viewsData->get($argument['table']); + $argument_definition = $argument_table_data[$argument['field']]['argument'] ?? []; + if (isset($argument_definition['id']) && $argument_definition['id'] === 'entity_target_id') { + $argument['plugin_id'] = 'entity_target_id'; + $argument['target_entity_type_id'] = $argument_definition['target_entity_type_id']; + $display['display_options']['arguments'][$argument_id] = $argument; + $changed = TRUE; + } + } + } + } + } + + if ($changed) { + $view->set('display', $displays); + } + + $deprecations_triggered = &$this->triggeredDeprecations['2640994'][$view->id()]; + if ($this->deprecationsEnabled && $changed && !$deprecations_triggered) { + $deprecations_triggered = TRUE; + @trigger_error(sprintf('The update to convert "numeric" arguments to "entity_target_id" for entity reference fields for view "%s" is deprecated in drupal:10.3.0 and is removed from drupal:12.0.0. Profile, module and theme provided configuration should be updated. See https://www.drupal.org/node/3441945', $view->id()), E_USER_DEPRECATED); + } + + return $changed; + } + } diff --git a/core/modules/views/tests/fixtures/update/entity-id-argument.php b/core/modules/views/tests/fixtures/update/entity-id-argument.php new file mode 100644 index 000000000000..9c18fa4c1ee1 --- /dev/null +++ b/core/modules/views/tests/fixtures/update/entity-id-argument.php @@ -0,0 +1,21 @@ +<?php + +/** + * @file + * Test fixture. + */ + +use Drupal\Core\Database\Database; +use Drupal\Component\Serialization\Yaml; + +$connection = Database::getConnection(); + +$config = Yaml::decode(file_get_contents(__DIR__ . '/views.view.test_entity_id_argument_update.yml')); + +$connection->insert('config') + ->fields([ + 'collection' => '', + 'name' => 'views.view.test_entity_id_argument_update', + 'data' => serialize($config), + ]) + ->execute(); diff --git a/core/modules/views/tests/fixtures/update/views.view.test_entity_id_argument_update.yml b/core/modules/views/tests/fixtures/update/views.view.test_entity_id_argument_update.yml new file mode 100644 index 000000000000..04081a72149a --- /dev/null +++ b/core/modules/views/tests/fixtures/update/views.view.test_entity_id_argument_update.yml @@ -0,0 +1,228 @@ +uuid: 06456283-a609-4646-aeb4-f2b918b13717 +langcode: en +status: true +dependencies: + config: + - core.entity_view_mode.node.teaser + - node.type.article + - taxonomy.vocabulary.tags + module: + - node + - taxonomy + - user +id: test_entity_id_argument_update +label: test_entity_id_argument_update +module: views +description: '' +tag: '' +base_table: node_field_data +base_field: nid +display: + default: + id: default + display_title: Master + display_plugin: default + position: 0 + display_options: + title: test_entity_id_argument_update + 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 + make_link: false + absolute: false + word_boundary: false + ellipsis: false + strip_tags: false + trim: false + html: 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_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: none + options: + offset: 0 + items_per_page: 0 + 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: + title: + id: title + table: node_field_data + field: title + relationship: none + group_type: group + admin_label: '' + entity_type: node + entity_field: title + plugin_id: standard + order: DESC + expose: + label: '' + exposed: false + arguments: + field_tags_target_id: + id: field_tags_target_id + table: node__field_tags + field: field_tags_target_id + relationship: none + group_type: group + admin_label: '' + plugin_id: numeric + default_action: ignore + exception: + value: all + title_enable: false + title: All + title_enable: true + title: 'test: title {{ arguments.field_tags_target_id }}, input {{ raw_arguments.field_tags_target_id }}' + default_argument_type: fixed + default_argument_options: + argument: '' + summary_options: + base_path: '' + count: true + override: false + items_per_page: 25 + summary: + sort_order: asc + number_of_records: 0 + format: default_summary + specify_validation: false + validate: + type: none + fail: 'not found' + validate_options: { } + break_phrase: true + not: false + 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: '' + type: + id: type + table: node_field_data + field: type + entity_type: node + entity_field: type + plugin_id: bundle + value: + article: article + style: + type: default + row: + type: 'entity:node' + options: + view_mode: teaser + query: + type: views_query + options: + query_comment: '' + disable_sql_rewrite: false + distinct: false + replica: false + query_tags: { } + relationships: { } + header: + area: + id: area + table: views + field: area + relationship: none + group_type: group + admin_label: '' + plugin_id: text + empty: false + content: + value: 'title {{ arguments.field_tags_target_id }}, input {{ raw_arguments.field_tags_target_id }}' + format: basic_html + tokenize: true + footer: { } + display_extenders: { } + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - user + - 'user.node_grants:view' + - user.permissions + tags: { } + page_1: + id: page_1 + display_title: Page + display_plugin: page + position: 1 + display_options: + display_extenders: { } + path: entity-id-argument-test + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - user + - 'user.node_grants:view' + - user.permissions + tags: { } diff --git a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_argument_default_current_user.yml b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_argument_default_current_user.yml index 538c7e472ca6..864f0b9d59a9 100644 --- a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_argument_default_current_user.yml +++ b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_argument_default_current_user.yml @@ -21,7 +21,7 @@ display: field: uid id: uid table: node_field_data - plugin_id: numeric + plugin_id: entity_target_id entity_type: node entity_field: uid cache: diff --git a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_argument_dependency.yml b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_argument_dependency.yml index ae03510f6eb5..031292a098bc 100644 --- a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_argument_dependency.yml +++ b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_argument_dependency.yml @@ -212,7 +212,7 @@ display: not: false entity_type: node entity_field: uid - plugin_id: numeric + plugin_id: entity_target_id display_extenders: { } page_1: display_plugin: page diff --git a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_entity_id_argument.yml b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_entity_id_argument.yml new file mode 100644 index 000000000000..c74d28db5ba6 --- /dev/null +++ b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_entity_id_argument.yml @@ -0,0 +1,227 @@ +langcode: en +status: true +dependencies: + config: + - core.entity_view_mode.node.teaser + - node.type.article + - taxonomy.vocabulary.tags + module: + - node + - taxonomy + - user +id: test_entity_id_argument +label: test_entity_id_argument +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: tag + 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: none + options: + items_per_page: 0 + offset: 0 + style: + type: default + row: + type: 'entity:node' + options: + view_mode: teaser + 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: + article: article + entity_type: node + entity_field: type + plugin_id: bundle + sorts: + title: + id: title + table: node_field_data + field: title + order: DESC + entity_type: node + entity_field: title + plugin_id: standard + relationship: none + group_type: group + admin_label: '' + exposed: false + expose: + label: '' + title: test_entity_id_argument + header: + area: + id: area + table: views + field: area + relationship: none + group_type: group + admin_label: '' + empty: false + tokenize: true + content: + value: 'title {{ arguments.field_views_testing_tags_target_id }}, input {{ raw_arguments.field_views_testing_tags_target_id }}' + format: basic_html + plugin_id: text + footer: { } + empty: { } + relationships: { } + arguments: + field_views_testing_tags_target_id: + id: field_views_testing_tags_target_id + table: node__field_views_testing_tags + field: field_views_testing_tags_target_id + relationship: none + group_type: group + admin_label: '' + default_action: ignore + exception: + value: all + title_enable: false + title: All + title_enable: true + title: 'test: title {{ arguments.field_views_testing_tags_target_id }}, input {{ raw_arguments.field_views_testing_tags_target_id }}' + default_argument_type: fixed + default_argument_options: + argument: '' + summary_options: + base_path: '' + count: true + items_per_page: 25 + override: false + summary: + sort_order: asc + number_of_records: 0 + format: default_summary + specify_validation: false + validate: + type: none + fail: 'not found' + validate_options: { } + break_phrase: true + not: false + plugin_id: entity_target_id + display_extenders: { } + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - user + - 'user.node_grants:view' + - user.permissions + tags: { } + page_1: + display_plugin: page + id: page_1 + display_title: Page + position: 1 + display_options: + display_extenders: { } + path: entity-id-argument-test + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - user + - 'user.node_grants:view' + - user.permissions + tags: { } diff --git a/core/modules/views/tests/src/Functional/Plugin/EntityArgumentTest.php b/core/modules/views/tests/src/Functional/Plugin/EntityArgumentTest.php new file mode 100644 index 000000000000..3e2d75050cf7 --- /dev/null +++ b/core/modules/views/tests/src/Functional/Plugin/EntityArgumentTest.php @@ -0,0 +1,85 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\views\Functional\Plugin; + +use Drupal\Tests\taxonomy\Functional\Views\TaxonomyTestBase; +use Drupal\user\UserInterface; +use Drupal\views\Tests\ViewTestData; +use Drupal\views\Views; + +/** + * Tests the handler of the view: entity target argument. + * + * @group views + * @see \Drupal\views\Plugin\views\argument\EntityArgument + */ +class EntityArgumentTest extends TaxonomyTestBase { + + /** + * Views used by this test. + * + * @var array + */ + public static array $testViews = ['test_entity_id_argument']; + + /** + * Modules to enable. + * + * @var array + */ + protected static $modules = ['node', 'taxonomy']; + + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + + /** + * A user with permission to administer taxonomy. + * + * @var \Drupal\user\UserInterface + */ + protected UserInterface $adminUser; + + /** + * {@inheritdoc} + */ + protected function setUp($import_test_views = TRUE, $modules = []): void { + parent::setUp($import_test_views, $modules); + ViewTestData::createTestViews(static::class, ['views_test_config']); + + // Create an administrative user. + $this->adminUser = $this->drupalCreateUser(['administer taxonomy', 'bypass node access']); + $this->drupalLogin($this->adminUser); + + } + + /** + * Tests the generated title of a view with an entity target argument. + */ + public function testArgumentTitle(): void { + $view = Views::getView('test_entity_id_argument'); + $assert_session = $this->assertSession(); + + // Test with single entity ID examples. + $this->drupalGet('/entity-id-argument-test'); + $assert_session->titleEquals($view->getTitle() . ' | Drupal'); + $this->drupalGet('/entity-id-argument-test/1'); + $assert_session->titleEquals('test: title ' . $this->term1->label() . ', input ' . $this->term1->id() . ' | Drupal'); + $this->drupalGet('/entity-id-argument-test/2'); + $assert_session->titleEquals('test: title ' . $this->term2->label() . ', input ' . $this->term2->id() . ' | Drupal'); + + // Test with multiple entity IDs examples. + $this->drupalGet('/entity-id-argument-test/1,2'); + $assert_session->titleEquals('test: title ' . $this->term1->label() . ', ' . $this->term2->label() . ', input ' . $this->term1->id() . ',' . $this->term2->id() . ' | Drupal'); + $this->drupalGet('/entity-id-argument-test/2,1'); + $assert_session->titleEquals('test: title ' . $this->term2->label() . ', ' . $this->term1->label() . ', input ' . $this->term2->id() . ',' . $this->term1->id() . ' | Drupal'); + $this->drupalGet('/entity-id-argument-test/1+2'); + $assert_session->titleEquals('test: title ' . $this->term1->label() . ' + ' . $this->term2->label() . ', input ' . $this->term1->id() . '+' . $this->term2->id() . ' | Drupal'); + $this->drupalGet('/entity-id-argument-test/2+1'); + $assert_session->titleEquals('test: title ' . $this->term2->label() . ' + ' . $this->term1->label() . ', input ' . $this->term2->id() . '+' . $this->term1->id() . ' | Drupal'); + } + +} diff --git a/core/modules/views/tests/src/Functional/Update/EntityArgumentUpdateTest.php b/core/modules/views/tests/src/Functional/Update/EntityArgumentUpdateTest.php new file mode 100644 index 000000000000..d7a96775c55d --- /dev/null +++ b/core/modules/views/tests/src/Functional/Update/EntityArgumentUpdateTest.php @@ -0,0 +1,47 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\views\Functional\Update; + +use Drupal\FunctionalTests\Update\UpdatePathTestBase; +use Drupal\views\Entity\View; + +/** + * Tests the upgrade path for converting numeric arguments to entity_target_id. + * + * @group Update + * + * @see views_post_update_views_data_argument_plugin_id() + */ +class EntityArgumentUpdateTest extends UpdatePathTestBase { + + /** + * {@inheritdoc} + */ + protected function setDatabaseDumpFiles(): void { + $this->databaseDumpFiles = [ + __DIR__ . '/../../../../../system/tests/fixtures/update/drupal-10.3.0.filled.standard.php.gz', + __DIR__ . '/../../../fixtures/update/entity-id-argument.php', + ]; + } + + /** + * Tests that numeric argument plugins are updated properly. + */ + public function testViewsFieldPluginConversion(): void { + $view = View::load('test_entity_id_argument_update'); + $data = $view->toArray(); + $this->assertEquals('numeric', $data['display']['default']['display_options']['arguments']['field_tags_target_id']['plugin_id']); + $this->assertArrayNotHasKey('target_entity_type_id', $data['display']['default']['display_options']['arguments']['field_tags_target_id']); + + $this->runUpdates(); + + $view = View::load('test_entity_id_argument_update'); + $data = $view->toArray(); + $this->assertEquals('entity_target_id', $data['display']['default']['display_options']['arguments']['field_tags_target_id']['plugin_id']); + $this->assertEquals('taxonomy_term', $data['display']['default']['display_options']['arguments']['field_tags_target_id']['target_entity_type_id']); + + } + +} diff --git a/core/modules/views/tests/src/Kernel/Entity/EntityViewsDataTest.php b/core/modules/views/tests/src/Kernel/Entity/EntityViewsDataTest.php index 6488c1fe2ebd..22ecad80787b 100644 --- a/core/modules/views/tests/src/Kernel/Entity/EntityViewsDataTest.php +++ b/core/modules/views/tests/src/Kernel/Entity/EntityViewsDataTest.php @@ -787,7 +787,8 @@ protected function assertLanguageField(array $data): void { protected function assertEntityReferenceField(array $data): void { $this->assertEquals('field', $data['field']['id']); $this->assertEquals('numeric', $data['filter']['id']); - $this->assertEquals('numeric', $data['argument']['id']); + $this->assertEquals('entity_target_id', $data['argument']['id']); + $this->assertEquals('user', $data['argument']['target_entity_type_id']); $this->assertEquals('standard', $data['sort']['id']); } diff --git a/core/modules/views/tests/src/Kernel/ViewExecutableTest.php b/core/modules/views/tests/src/Kernel/ViewExecutableTest.php index 5afd8a1c7bb7..64aeeac346ac 100644 --- a/core/modules/views/tests/src/Kernel/ViewExecutableTest.php +++ b/core/modules/views/tests/src/Kernel/ViewExecutableTest.php @@ -8,6 +8,7 @@ use Drupal\Component\Utility\Xss; use Drupal\Core\Database\Database; use Drupal\node\Entity\NodeType; +use Drupal\user\Entity\User; use Drupal\views\Entity\View; use Drupal\views\Views; use Drupal\views\ViewExecutable; @@ -549,13 +550,17 @@ public function testSerialization() { * Tests if argument overrides by validators are propagated to tokens. */ public function testArgumentValidatorValueOverride() { + $account = User::create(['name' => $this->randomString()]); + $account->save(); + $view = Views::getView('test_argument_dependency'); $view->setDisplay('page_1'); - $view->setArguments(['1', 'this value should be replaced']); + $view->setArguments([(string) $account->id(), 'this value should be replaced']); $view->execute(); + $account = User::load(1); $expected = [ - '{{ arguments.uid }}' => '1', - '{{ raw_arguments.uid }}' => '1', + '{{ arguments.uid }}' => $account->label(), + '{{ raw_arguments.uid }}' => (string) $account->id(), ]; $this->assertEquals($expected, $view->build_info['substitutions']); } diff --git a/core/modules/views/views.post_update.php b/core/modules/views/views.post_update.php index 79c5c61aeb17..136a82ed193c 100644 --- a/core/modules/views/views.post_update.php +++ b/core/modules/views/views.post_update.php @@ -5,6 +5,10 @@ * Post update functions for Views. */ +use Drupal\Core\Config\Entity\ConfigEntityUpdater; +use Drupal\views\ViewEntityInterface; +use Drupal\views\ViewsConfigUpdater; + /** * Implements hook_removed_post_updates(). */ @@ -48,3 +52,15 @@ function views_removed_post_updates() { 'views_post_update_rendered_entity_field_cache_metadata' => '11.0.0', ]; } + +/** + * Post update configured views for entity reference argument plugin IDs. + */ +function views_post_update_views_data_argument_plugin_id(?array &$sandbox = NULL): void { + /** @var \Drupal\views\ViewsConfigUpdater $view_config_updater */ + $view_config_updater = \Drupal::classResolver(ViewsConfigUpdater::class); + $view_config_updater->setDeprecationsEnabled(FALSE); + \Drupal::classResolver(ConfigEntityUpdater::class)->update($sandbox, 'view', function (ViewEntityInterface $view) use ($view_config_updater): bool { + return $view_config_updater->needsEntityArgumentUpdate($view); + }); +} diff --git a/core/modules/views/views.views.inc b/core/modules/views/views.views.inc index e24be15b906e..ebde44706bbb 100644 --- a/core/modules/views/views.views.inc +++ b/core/modules/views/views.views.inc @@ -767,9 +767,10 @@ function views_field_default_views_data(FieldStorageConfigInterface $field_stora /** * Implements hook_field_views_data(). * - * The function implements the hook in behalf of 'core' because it adds a + * The function implements the hook on behalf of 'core' because it adds a * relationship and a reverse relationship to entity_reference field type, which - * is provided by core. + * is provided by core. This function also provides an argument plugin for + * entity_reference fields that handles title token replacement. */ function core_field_views_data(FieldStorageConfigInterface $field_storage) { $data = views_field_default_views_data($field_storage); @@ -838,6 +839,11 @@ function core_field_views_data(FieldStorageConfigInterface $field_storage) { ], ]; } + + // Provide an argument plugin that has a meaningful titleQuery() + // implementation getting the entity label. + $data[$table_name][$field_name . '_target_id']['argument']['id'] = 'entity_target_id'; + $data[$table_name][$field_name . '_target_id']['argument']['target_entity_type_id'] = $target_entity_type_id; } return $data; diff --git a/core/profiles/demo_umami/config/install/views.view.related_recipes.yml b/core/profiles/demo_umami/config/install/views.view.related_recipes.yml index 3c46f66b996e..8ee77d538ede 100644 --- a/core/profiles/demo_umami/config/install/views.view.related_recipes.yml +++ b/core/profiles/demo_umami/config/install/views.view.related_recipes.yml @@ -153,7 +153,8 @@ display: relationship: none group_type: group admin_label: '' - plugin_id: numeric + plugin_id: entity_target_id + target_entity_type_id: taxonomy_term default_action: default exception: value: all -- GitLab