Issue #2826826 by vasike, dpi, raman.b, rpayanm, jibran, gpap, mpolishchuck, rwohleb, ranjith_kumar_k_u, smustgrave, johnnydarkko, mrinalini9, Zarpele, Berdir, amateescu, hchonov, amitaibu, larowlan, heddn, RoySegall, quietone: Entity autocomplete widget does not pass along entity to AJAX request

merge-requests/3189/merge
Lee Rowlands 2023-07-10 10:24:03 +10:00
parent 08bde872c0
commit 91f59e7033
No known key found for this signature in database
GPG Key ID: 2B829A3DF9204DC4
8 changed files with 156 additions and 8 deletions

View File

@ -173,6 +173,12 @@ class EntityAutocomplete extends Textfield {
// Store the selection settings in the key/value store and pass a hashed key
// in the route parameters.
$selection_settings = $element['#selection_settings'] ?? [];
// Don't serialize the entity, it will be added explicitly afterwards.
if (isset($selection_settings['entity']) && ($selection_settings['entity'] instanceof EntityInterface)) {
$element['#autocomplete_query_parameters']['entity_type'] = $selection_settings['entity']->getEntityTypeId();
$element['#autocomplete_query_parameters']['entity_id'] = $selection_settings['entity']->id();
unset($selection_settings['entity']);
}
$data = serialize($selection_settings) . $element['#target_type'] . $element['#selection_handler'];
$selection_settings_key = Crypt::hmacBase64($data, Settings::getHashSalt());

View File

@ -103,6 +103,11 @@ class EntityReferenceAutocompleteWidget extends WidgetBase {
'match_limit' => $this->getSetting('match_limit'),
];
// Append the entity if it is already created.
if (!$entity->isNew()) {
$selection_settings['entity'] = $entity;
}
$element += [
'#type' => 'entity_autocomplete',
'#target_type' => $this->getFieldSetting('target_type'),

View File

@ -157,7 +157,7 @@ abstract class FormElement extends RenderElement implements FormElementInterface
*
* This sets up autocomplete functionality for elements with an
* #autocomplete_route_name property, using the #autocomplete_route_parameters
* property if present.
* and #autocomplete_query_parameters properties if present.
*
* For example, suppose your autocomplete route name is
* 'mymodule.autocomplete' and its path is
@ -176,6 +176,8 @@ abstract class FormElement extends RenderElement implements FormElementInterface
* autocomplete JavaScript library.
* - #autocomplete_route_parameters: The parameters to be used in
* conjunction with the route name.
* - #autocomplete_query_parameters: The parameters to be used in
* query string
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param array $complete_form
@ -190,7 +192,11 @@ abstract class FormElement extends RenderElement implements FormElementInterface
if (!empty($element['#autocomplete_route_name'])) {
$parameters = $element['#autocomplete_route_parameters'] ?? [];
$url = Url::fromRoute($element['#autocomplete_route_name'], $parameters)->toString(TRUE);
$options = [];
if (!empty($element['#autocomplete_query_parameters'])) {
$options['query'] = $element['#autocomplete_query_parameters'];
}
$url = Url::fromRoute($element['#autocomplete_route_name'], $parameters, $options)->toString(TRUE);
/** @var \Drupal\Core\Access\AccessManagerInterface $access_manager */
$access_manager = \Drupal::service('access_manager');
$access = $access_manager->checkNamedRoute($element['#autocomplete_route_name'], $parameters, \Drupal::currentUser(), TRUE);

View File

@ -99,6 +99,17 @@ class EntityAutocompleteController extends ControllerBase {
throw new AccessDeniedHttpException();
}
$entity_type_id = $request->query->get('entity_type');
if ($entity_type_id && $this->entityTypeManager()->hasDefinition($entity_type_id)) {
$entity_id = $request->query->get('entity_id');
if ($entity_id) {
$entity = $this->entityTypeManager()->getStorage($entity_type_id)->load($entity_id);
if ($entity->access('update')) {
$selection_settings['entity'] = $entity;
}
}
}
$matches = $this->matcher->getMatches($target_type, $selection_handler, $selection_settings, $typed_string);
}

View File

@ -0,0 +1,4 @@
# Schema for the entity reference 'entity_test_all_except_host' selection
# handler settings.
entity_reference_selection.entity_test_all_except_host:
type: entity_reference_selection.default

View File

@ -0,0 +1,34 @@
<?php
namespace Drupal\entity_reference_test\Plugin\EntityReferenceSelection;
use Drupal\Core\Entity\Plugin\EntityReferenceSelection\DefaultSelection;
/**
* Allows access to all entities except for the host entity.
*
* @EntityReferenceSelection(
* id = "entity_test_all_except_host",
* label = @Translation("All except host entity."),
* group = "entity_test_all_except_host"
* )
*/
class AllExceptHostEntity extends DefaultSelection {
/**
* {@inheritdoc}
*/
protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') {
$query = parent::buildEntityQuery($match, $match_operator);
/** @var \Drupal\Core\Entity\EntityInterface $entity */
if ($entity = $this->configuration['entity']) {
$target_type = $this->configuration['target_type'];
$entity_type = $this->entityTypeManager->getDefinition($target_type);
$query->condition($entity_type->getKey('id'), $entity->id(), '<>');
}
return $query;
}
}

View File

@ -4,6 +4,8 @@ namespace Drupal\FunctionalJavascriptTests\EntityReference;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\Tests\field\Traits\EntityReferenceTestTrait;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
use Drupal\Tests\node\Traits\NodeCreationTrait;
@ -21,7 +23,12 @@ class EntityReferenceAutocompleteWidgetTest extends WebDriverTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['node', 'field_ui'];
protected static $modules = [
'node',
'field_ui',
'entity_test',
'entity_reference_test',
];
/**
* {@inheritdoc}
@ -149,6 +156,49 @@ class EntityReferenceAutocompleteWidgetTest extends WebDriverTestBase {
$this->assertCount(2, $page->findAll('css', '.ui-autocomplete li'));
}
/**
* Tests that the autocomplete widget knows about the entity its attached to.
*
* Ensures that the entity the autocomplete widget stores the entity it is
* rendered on, and is available in the autocomplete results' AJAX request.
*/
public function testEntityReferenceAutocompleteWidgetAttachedEntity() {
$user = $this->drupalCreateUser([
'administer entity_test content',
]);
$this->drupalLogin($user);
$field_name = 'field_test';
$this->createEntityReferenceField('entity_test', 'entity_test', $field_name, $field_name, 'entity_test', 'entity_test_all_except_host', ['target_bundles' => ['entity_test']]);
$form_display = EntityFormDisplay::load('entity_test.entity_test.default');
$form_display->setComponent($field_name, [
'type' => 'entity_reference_autocomplete',
'settings' => [
'match_operator' => 'CONTAINS',
],
]);
$form_display->save();
$host = EntityTest::create(['name' => 'dark green']);
$host->save();
EntityTest::create(['name' => 'dark blue'])->save();
$this->drupalGet($host->toUrl('edit-form'));
// Trigger the autocomplete.
$page = $this->getSession()->getPage();
$autocomplete_field = $page->findField($field_name . '[0][target_id]');
$autocomplete_field->setValue('dark');
$this->getSession()->getDriver()->keyDown($autocomplete_field->getXpath(), ' ');
$this->assertSession()->waitOnAutocomplete();
// Check the autocomplete results.
$results = $page->findAll('css', '.ui-autocomplete li');
$this->assertCount(1, $results);
$this->assertSession()->elementTextNotContains('css', '.ui-autocomplete li', 'dark green');
$this->assertSession()->elementTextContains('css', '.ui-autocomplete li', 'dark blue');
}
/**
* Executes an autocomplete on a given field and waits for it to finish.
*

View File

@ -18,6 +18,13 @@ use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
*/
class EntityAutocompleteTest extends EntityKernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'entity_reference_test',
];
/**
* The entity type used in this test.
*
@ -71,6 +78,16 @@ class EntityAutocompleteTest extends EntityKernelTestBase {
];
$this->assertSame($target, reset($data), 'Autocomplete returns only the expected matching entity.');
// Pass the first entity to the request.
// We should get empty results.
// First we need to have permission to pass entity.
$user = $this->drupalCreateUser([
'administer entity_test content',
]);
$this->drupalSetCurrentUser($user);
$data = $this->getAutocompleteResult($input, $entity_1->id());
$this->assertSame([], $data, 'Autocomplete returns empty results as first entity is passed to autocomplete request.');
// Try to autocomplete an entity label that matches the second entity, and
// the first entity is already typed in the autocomplete (tags) widget.
$input = $entity_1->name->value . ' (1), 10/17';
@ -99,6 +116,13 @@ class EntityAutocompleteTest extends EntityKernelTestBase {
$this->assertSame(Html::escape($entity_2->name->value), $data[1]['label'], 'Autocomplete returned the second matching entity');
$this->assertSame(Html::escape($entity_3->name->value), $data[2]['label'], 'Autocomplete returned the third matching entity');
// Pass the first entity to the request.
// We should not get the first entity in the results.
$data = $this->getAutocompleteResult($input, $entity_1->id());
$this->assertCount(2, $data, 'Autocomplete returned only 2 entities');
$this->assertSame(Html::escape($entity_2->name->value), $data[0]['label'], 'Autocomplete returned the second matching entity');
$this->assertSame(Html::escape($entity_3->name->value), $data[1]['label'], 'Autocomplete returned the third matching entity');
// Strange input that is mangled by
// \Drupal\Component\Utility\Tags::explode().
$input = '"l!J>&Tw';
@ -149,20 +173,28 @@ class EntityAutocompleteTest extends EntityKernelTestBase {
*
* @param string $input
* The label of the entity to query by.
* @param int $entity_id
* The label of the entity to query by.
*
* @return mixed
* The JSON value encoded in its appropriate PHP type.
*/
protected function getAutocompleteResult($input) {
$request = Request::create('entity_reference_autocomplete/' . $this->entityType . '/default');
protected function getAutocompleteResult($input, $entity_id = NULL) {
// Use "entity_test_all_except_host" EntityReferenceSelection
// to also test passing an entity to autocomplete requests.
$request = Request::create('entity_reference_autocomplete/' . $this->entityType . '/entity_test_all_except_host');
$request->query->set('q', $input);
$selection_settings = [];
$selection_settings_key = Crypt::hmacBase64(serialize($selection_settings) . $this->entityType . 'default', Settings::getHashSalt());
if ($entity_id) {
$request->query->set('entity_type', $this->entityType);
$request->query->set('entity_id', $entity_id);
}
$selection_settings_key = Crypt::hmacBase64(serialize($selection_settings) . $this->entityType . 'entity_test_all_except_host', Settings::getHashSalt());
\Drupal::keyValue('entity_autocomplete')->set($selection_settings_key, $selection_settings);
$entity_reference_controller = EntityAutocompleteController::create($this->container);
$result = $entity_reference_controller->handleAutocomplete($request, $this->entityType, 'default', $selection_settings_key)->getContent();
$result = $entity_reference_controller->handleAutocomplete($request, $this->entityType, 'entity_test_all_except_host', $selection_settings_key)->getContent();
return Json::decode($result);
}