diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/MachineNameWidget.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/MachineNameWidget.php index c6c5716..f1a40d3 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/MachineNameWidget.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/MachineNameWidget.php @@ -71,9 +71,6 @@ public static function defaultSettings() { return [ 'source_field' => '', 'disable_on_edit' => TRUE, - // The 'exists' property is not configurable in the UI but it can be set - // manually in code. - 'exists' => '', 'replace_pattern' => '[^a-z0-9_]+', 'replace' => '_', ] + parent::defaultSettings(); @@ -161,7 +158,7 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen '#source_field' => $this->getSetting('source_field'), '#process' => array_merge([[get_class($this), 'processMachineNameSource']], $element_info['#process']), '#machine_name' => [ - 'exists' => $this->getSetting('exists'), + 'exists' => [get_called_class(), 'exists'], 'label' => $this->fieldDefinition->getLabel(), 'replace_pattern' => $this->getSetting('replace_pattern'), 'replace' => $this->getSetting('replace'), @@ -193,4 +190,35 @@ public static function processMachineNameSource($element, FormStateInterface $fo return $element; } + /** + * Determines if the source field value already exists. + * + * @param mixed $value + * The value entered into the machine name widget. + * @param array $element + * The form element. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @return bool + * TRUE if the source field value already exists, FALSE otherwise. + */ + public static function exists($value, array $element, FormStateInterface $form_state) { + return (bool) \Drupal::entityTypeManager() + ->getStorage($form_state->getFormObject()->getEntity()->getEntityTypeId()) + ->getQuery() + ->condition(reset($element['#array_parents']), $value, '=') + ->execute(); + } + + /** + * {@inheritdoc} + */ + public static function isApplicable(FieldDefinitionInterface $field_definition) { + // This widget is available only to entity identifier fields. + $entity_type = \Drupal::entityTypeManager()->getDefinition($field_definition->getTargetEntityTypeId()); + + return $field_definition->getName() === $entity_type->getKey('id'); + } + } diff --git a/core/lib/Drupal/Core/Render/Element/MachineName.php b/core/lib/Drupal/Core/Render/Element/MachineName.php index 4179207..ace452b 100644 --- a/core/lib/Drupal/Core/Render/Element/MachineName.php +++ b/core/lib/Drupal/Core/Render/Element/MachineName.php @@ -247,7 +247,7 @@ public static function validateMachineName(&$element, FormStateInterface $form_s } // Verify that the machine name is unique. - if ($element['#default_value'] !== $element['#value'] && !empty($element['#machine_name']['exists'])) { + if ($element['#default_value'] !== $element['#value']) { $function = $element['#machine_name']['exists']; if (call_user_func($function, $element['#value'], $element, $form_state)) { $form_state->setError($element, t('The machine-readable name is already in use. It must be unique.')); diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestStringId.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestStringId.php index 85a5e66..05c322e 100644 --- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestStringId.php +++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestStringId.php @@ -50,7 +50,12 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ->setReadOnly(TRUE) // In order to work around the InnoDB 191 character limit on utf8mb4 // primary keys, we set the character set for the field to ASCII. - ->setSetting('is_ascii', TRUE); + ->setSetting('is_ascii', TRUE) + ->setDisplayConfigurable('form', TRUE); + + // Make the label field configurable in the UI. + $fields['name']->setDisplayConfigurable('form', TRUE); + return $fields; } diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Core/Field/Widget/MachineNameWidgetTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Core/Field/Widget/MachineNameWidgetTest.php index acc085b..e7313f2 100644 --- a/core/tests/Drupal/FunctionalJavascriptTests/Core/Field/Widget/MachineNameWidgetTest.php +++ b/core/tests/Drupal/FunctionalJavascriptTests/Core/Field/Widget/MachineNameWidgetTest.php @@ -2,9 +2,7 @@ namespace Drupal\FunctionalJavascriptTests\Core\Field\Widget; -use Drupal\entity_test\Entity\EntityTest; -use Drupal\field\Entity\FieldConfig; -use Drupal\field\Entity\FieldStorageConfig; +use Drupal\entity_test\Entity\EntityTestStringId; use Drupal\FunctionalJavascriptTests\JavascriptTestBase; /** @@ -25,50 +23,13 @@ class MachineNameWidgetTest extends JavascriptTestBase { public static $modules = ['entity_test', 'field', 'field_ui', 'node']; /** - * A field storage to use as a 'source' field for the machine name widget. - * - * @var \Drupal\field\Entity\FieldStorageConfig - */ - protected $testFieldSource; - - /** - * A field storage to use with the machine name widget. - * - * @var \Drupal\field\Entity\FieldStorageConfig - */ - protected $testField; - - /** * {@inheritdoc} */ protected function setUp() { parent::setUp(); - // Add two string fields to the entity_test entity type. - $this->testFieldSource = FieldStorageConfig::create([ - 'field_name' => 'test_field_source', - 'entity_type' => 'entity_test', - 'type' => 'string', - ]); - $this->testFieldSource->save(); - FieldConfig::create([ - 'field_storage' => $this->testFieldSource, - 'bundle' => 'entity_test', - ])->save(); - - $this->testField = FieldStorageConfig::create([ - 'field_name' => 'test_field', - 'entity_type' => 'entity_test', - 'type' => 'string', - ]); - $this->testField->save(); - FieldConfig::create([ - 'field_storage' => $this->testField, - 'bundle' => 'entity_test', - ])->save(); - // Create a web user. - $this->drupalLogin($this->drupalCreateUser(['view test entity', 'administer entity_test content', 'administer entity_test form display'])); + $this->drupalLogin($this->drupalCreateUser(['view test entity', 'administer entity_test content', 'administer entity_test_string_id form display'])); } /** @@ -78,16 +39,23 @@ public function testMachineNameWidget() { $assert_session = $this->assertSession(); $page = $this->getSession()->getPage(); + // First, make sure that both the ID and the label fields are initially + // hidden in the form display. + entity_get_form_display('entity_test_string_id', 'entity_test_string_id', 'default') + ->removeComponent('id') + ->removeComponent('name') + ->save(); + // Go to the entity add form and check that the test fields are not // displayed because they are not configured in the form display yet. - $this->drupalGet('/entity_test/add'); - $assert_session->fieldNotExists('test_field[0][value]'); - $assert_session->fieldNotExists('test_field_source[0][value]'); + $this->drupalGet('/entity_test_string_id/add'); + $assert_session->fieldNotExists('id[0][value]'); + $assert_session->fieldNotExists('name[0][value]'); // Configure the test field to use the machine name widget with no initial // settings. - entity_get_form_display('entity_test', 'entity_test', 'default') - ->setComponent($this->testField->getName(), [ + entity_get_form_display('entity_test_string_id', 'entity_test_string_id', 'default') + ->setComponent('id', [ 'type' => 'machine_name', 'weight' => 5, ]) @@ -95,46 +63,53 @@ public function testMachineNameWidget() { // Check that the widget displays an "error" summary when it has missing or // broken settings. - $this->drupalGet('/entity_test/structure/entity_test/form-display'); + $this->drupalGet('/entity_test_string_id/structure/entity_test_string_id/form-display'); $assert_session->pageTextContains('Missing configuration.'); // Check that test field is configured in the form display while the source // field is not. - $this->assertTrue($assert_session->optionExists('fields[test_field][region]', 'content')->isSelected()); - $this->assertTrue($assert_session->optionExists('fields[test_field_source][region]', 'hidden')->isSelected()); + $this->assertTrue($assert_session->optionExists('fields[id][region]', 'content')->isSelected()); + $this->assertTrue($assert_session->optionExists('fields[name][region]', 'hidden')->isSelected()); // Open the widget settings form and check that a field which is not present // in the form display can not be selected as a 'source_field'. - $page->pressButton('test_field_settings_edit'); - $assert_session->waitForField('fields[test_field][settings_edit_form][settings][source_field]'); - $assert_session->optionNotExists('fields[test_field][settings_edit_form][settings][source_field]', 'test_field_source'); + $page->pressButton('id_settings_edit'); + $assert_session->waitForField('fields[id][settings_edit_form][settings][source_field]'); + $assert_session->optionNotExists('fields[id][settings_edit_form][settings][source_field]', 'name'); // Go to the entity add form and check that the test field is not displayed, // even when it is enabled in the form display, because of missing settings. - $this->drupalGet('/entity_test/add'); - $assert_session->fieldNotExists('test_field[0][value]'); - $assert_session->fieldNotExists('test_field_source[0][value]'); + $this->drupalGet('/entity_test_string_id/add'); + $assert_session->fieldNotExists('id[0][value]'); + $assert_session->fieldNotExists('name[0][value]'); // Enable the 'source' field in the entity form display and configure the // test field to use it. - entity_get_form_display('entity_test', 'entity_test', 'default') - ->setComponent($this->testFieldSource->getName(), [ + entity_get_form_display('entity_test_string_id', 'entity_test_string_id', 'default') + ->setComponent('name', [ 'type' => 'string_textfield', 'weight' => 10, ]) ->save(); - // Go to the form display and configure the test field to use the newly - // enabled 'source' field as the machine name source. - $this->drupalGet('/entity_test/structure/entity_test/form-display'); - - $page->pressButton('test_field_settings_edit'); - $assert_session->waitForField('fields[test_field][settings_edit_form][settings][source_field]'); - $page->selectFieldOption('fields[test_field][settings_edit_form][settings][source_field]', 'test_field_source'); - $page->pressButton('test_field_plugin_settings_update'); + // Go to the form display and check that the machine name widget is only + // available for the ID field. + $this->drupalGet('/entity_test_string_id/structure/entity_test_string_id/form-display'); + $assert_session->optionExists('edit-fields-name-type', 'string_textfield'); + $assert_session->optionNotExists('edit-fields-name-type', 'machine_name'); + + $assert_session->optionExists('edit-fields-id-type', 'string_textfield'); + $assert_session->optionExists('edit-fields-id-type', 'machine_name'); + + // Configure the test field to use the newly enabled 'source' field as the + // machine name source. + $page->pressButton('id_settings_edit'); + $assert_session->waitForField('fields[id][settings_edit_form][settings][source_field]'); + $page->selectFieldOption('fields[id][settings_edit_form][settings][source_field]', 'name'); + $page->pressButton('id_plugin_settings_update'); $assert_session->assertWaitOnAjaxRequest(); - $assert_session->pageTextContains('Source field: test_field_source'); + $assert_session->pageTextContains('Source field: Name'); $assert_session->pageTextContains('Replace pattern: [^a-z0-9_]+'); $assert_session->pageTextContains('Replace character: _'); @@ -145,26 +120,26 @@ public function testMachineNameWidget() { // displayed, even when it has a source field configured, because the weight // of the source field in the form display is higher than the weight of the // test field. - $this->drupalGet('/entity_test/add'); - $assert_session->fieldNotExists('test_field[0][value]'); - $assert_session->fieldExists('test_field_source[0][value]'); + $this->drupalGet('/entity_test_string_id/add'); + $assert_session->fieldNotExists('id[0][value]'); + $assert_session->fieldExists('name[0][value]'); // Configure the source field to have a lower weight than the test field, // which will finally make it appear in the entity form. - entity_get_form_display('entity_test', 'entity_test', 'default') - ->setComponent($this->testFieldSource->getName(), [ + entity_get_form_display('entity_test_string_id', 'entity_test_string_id', 'default') + ->setComponent('name', [ 'type' => 'string_textfield', 'weight' => 4, ]) ->save(); - $this->drupalGet('/entity_test/add'); - $test_source_field = $page->findField('test_field_source[0][value]'); - $test_field = $page->findField('test_field[0][value]'); - $test_field_machine_name_value = $page->find('css', '#edit-test-field-source-0-value-machine-name-suffix .machine-name-value'); + $this->drupalGet('/entity_test_string_id/add'); + $test_source_field = $page->findField('name[0][value]'); + $id = $page->findField('id[0][value]'); + $id_machine_name_value = $page->find('css', '#edit-name-0-value-machine-name-suffix .machine-name-value'); $this->assertNotEmpty($test_source_field); - $this->assertNotEmpty($test_field); - $this->assertNotEmpty($test_field_machine_name_value, 'Test field with the machine name widget has been initialized.'); + $this->assertNotEmpty($id); + $this->assertNotEmpty($id_machine_name_value, 'Test field with the machine name widget has been initialized.'); $test_values = [ 'input' => 'Test value !0-9@', @@ -173,36 +148,36 @@ public function testMachineNameWidget() { $test_source_field->setValue($test_values['input']); // Wait the set timeout for fetching the machine name. - $this->assertJsCondition('jQuery("#edit-test-field-source-0-value-machine-name-suffix .machine-name-value").html() == "' . $test_values['expected'] . '"'); + $this->assertJsCondition('jQuery("#edit-name-0-value-machine-name-suffix .machine-name-value").html() == "' . $test_values['expected'] . '"'); // Validate the generated machine name. - $this->assertEquals($test_values['expected'], $test_field_machine_name_value->getHtml()); + $this->assertEquals($test_values['expected'], $id_machine_name_value->getHtml()); // Submit the entity form. $this->submitForm([], 'Save'); // Load the entity and check that machine name value that was saved is // correct. - $entity = EntityTest::load(1); - $this->assertSame($test_values['input'], $entity->test_field_source->value); - $this->assertSame($test_values['expected'], $entity->test_field->value); + $entity = EntityTestStringId::load('test_value_0_9_'); + $this->assertSame($test_values['input'], $entity->name->value); + $this->assertSame($test_values['expected'], $entity->id->value); // Try changing the 'replace_pattern' and 'replace' settings of the widget. - entity_get_form_display('entity_test', 'entity_test', 'default') - ->setComponent($this->testField->getName(), [ + entity_get_form_display('entity_test_string_id', 'entity_test_string_id', 'default') + ->setComponent('id', [ 'type' => 'machine_name', 'weight' => 5, 'settings' => [ - 'source_field' => $this->testFieldSource->getName(), + 'source_field' => 'name', 'replace_pattern' => '[^a-z0-9-]+', 'replace' => '-', ], ]) ->save(); - $this->drupalGet('/entity_test/add'); - $test_source_field = $page->findField('test_field_source[0][value]'); - $test_field_machine_name_value = $page->find('css', '#edit-test-field-source-0-value-machine-name-suffix .machine-name-value'); + $this->drupalGet('/entity_test_string_id/add'); + $test_source_field = $page->findField('name[0][value]'); + $id_machine_name_value = $page->find('css', '#edit-name-0-value-machine-name-suffix .machine-name-value'); $test_values = [ 'input' => 'Test value2 !0-9@', @@ -211,19 +186,43 @@ public function testMachineNameWidget() { $test_source_field->setValue($test_values['input']); // Wait the set timeout for fetching the machine name. - $this->assertJsCondition('jQuery("#edit-test-field-source-0-value-machine-name-suffix .machine-name-value").html() == "' . $test_values['expected'] . '"'); + $this->assertJsCondition('jQuery("#edit-name-0-value-machine-name-suffix .machine-name-value").html() == "' . $test_values['expected'] . '"'); // Validate the generated machine name. - $this->assertEquals($test_values['expected'], $test_field_machine_name_value->getHtml()); + $this->assertEquals($test_values['expected'], $id_machine_name_value->getHtml()); // Submit the entity form. $this->submitForm([], 'Save'); // Load the entity and check that machine name value that was saved is // correct. - $entity = EntityTest::load(2); - $this->assertSame($test_values['input'], $entity->test_field_source->value); - $this->assertSame($test_values['expected'], $entity->test_field->value); + $entity = EntityTestStringId::load('test-value2-0-9-'); + $this->assertSame($test_values['input'], $entity->name->value); + $this->assertSame($test_values['expected'], $entity->id->value); + + // Repeat the steps above in order to check that entering an existing value + // in the machine name widget throws an error. + $this->drupalGet('/entity_test_string_id/add'); + $test_source_field = $page->findField('name[0][value]'); + $id_machine_name_value = $page->find('css', '#edit-name-0-value-machine-name-suffix .machine-name-value'); + + $test_values = [ + 'input' => 'Test value2 !0-9@', + 'expected' => 'test-value2-0-9-', + ]; + $test_source_field->setValue($test_values['input']); + + // Wait the set timeout for fetching the machine name. + $this->assertJsCondition('jQuery("#edit-name-0-value-machine-name-suffix .machine-name-value").html() == "' . $test_values['expected'] . '"'); + + // Validate the generated machine name. + $this->assertEquals($test_values['expected'], $id_machine_name_value->getHtml()); + + // Submit the entity form. + $this->submitForm([], 'Save'); + + // Check that a form-level error has been thrown. + $assert_session->pageTextContains('The machine-readable name is already in use. It must be unique.'); } }