diff --git a/core/config/schema/core.entity.schema.yml b/core/config/schema/core.entity.schema.yml index d1adfd9fc2c..823480fa0d4 100644 --- a/core/config/schema/core.entity.schema.yml +++ b/core/config/schema/core.entity.schema.yml @@ -261,6 +261,29 @@ field.widget.settings.entity_reference_autocomplete: type: label label: 'Placeholder' +field.widget.settings.machine_name: + type: mapping + label: 'Machine Name' + mapping: + source_field: + type: string + label: 'The source field for the machine name' + disable_on_edit: + type: boolean + label: 'Disable the machine name after initial creation' + standalone: + type: boolean + label: 'Keep the machine name live preview as its own form element' + replace_pattern: + type: string + label: 'A regular expression (without delimiters) matching disallowed characters in the machine name' + replace: + type: string + label: 'A character to replace disallowed characters in the machine name' + size: + type: integer + label: 'Size of the machine name' + field.formatter.settings.boolean: type: mapping mapping: diff --git a/core/lib/Drupal/Core/Entity/Entity/EntityFormDisplay.php b/core/lib/Drupal/Core/Entity/Entity/EntityFormDisplay.php index 2384062319e..5dbb1ba4394 100644 --- a/core/lib/Drupal/Core/Entity/Entity/EntityFormDisplay.php +++ b/core/lib/Drupal/Core/Entity/Entity/EntityFormDisplay.php @@ -2,6 +2,7 @@ namespace Drupal\Core\Entity\Entity; +use Drupal\Component\Utility\SortArray; use Drupal\Core\Entity\EntityConstraintViolationListInterface; use Drupal\Core\Entity\EntityDisplayPluginCollection; use Drupal\Core\Entity\FieldableEntityInterface; @@ -175,8 +176,15 @@ public function buildForm(FieldableEntityInterface $entity, array &$form, FormSt // Set #parents to 'top-level' by default. $form += ['#parents' => []]; + // Sort the components by their weight in order to allow a form element from + // a widget to depend on the processed elements of another form element from + // another widget. For example, the 'machine_name' widget needs the + // processed '#id' property of its source field. + $components = $this->getComponents(); + uasort($components, SortArray::sortByWeightElement(...)); + // Let each widget generate the form elements. - foreach ($this->getComponents() as $name => $options) { + foreach ($components as $name => $options) { if ($widget = $this->getRenderer($name)) { $items = $entity->get($name); $items->filterEmptyItems(); diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/MachineNameWidget.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/MachineNameWidget.php new file mode 100644 index 00000000000..f10ec9c00ad --- /dev/null +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/MachineNameWidget.php @@ -0,0 +1,241 @@ +entityFieldManager = $entity_field_manager; + $this->elementInfo = $element_info; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $plugin_id, + $plugin_definition, + $configuration['field_definition'], + $configuration['settings'], + $configuration['third_party_settings'], + $container->get('entity_field.manager'), + $container->get('element_info') + ); + } + + /** + * {@inheritdoc} + */ + public static function defaultSettings() { + return [ + 'source_field' => '', + 'disable_on_edit' => TRUE, + 'standalone' => FALSE, + 'replace_pattern' => '[^a-z0-9_]+', + 'replace' => '_', + 'size' => 60, + ] + parent::defaultSettings(); + } + + /** + * {@inheritdoc} + */ + public function settingsForm(array $form, FormStateInterface $form_state) { + /** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $form_display */ + $form_display = $form_state->getFormObject()->getEntity(); + + $available_fields = $this->entityFieldManager->getFieldDefinitions($form_display->getTargetEntityTypeId(), $form_display->getTargetBundle()); + $displayed_fields = $form_display->getComponents(); + + $options = []; + /**@var \Drupal\Core\Field\FieldDefinitionInterface $field */ + foreach (array_intersect_key($available_fields, $displayed_fields) as $field_name => $field) { + // The source field can only be another string field and it has to be + // displayed in the form before the field that is using this widget. + if ($field->getType() === 'string' + && $field->getName() !== $this->fieldDefinition->getName() + && $displayed_fields[$field_name]['weight'] < $displayed_fields[$this->fieldDefinition->getName()]['weight']) { + $options[$field_name] = $field->getLabel(); + } + } + $element['source_field'] = [ + '#type' => 'select', + '#title' => $this->t('Source field'), + '#default_value' => $this->getSetting('source_field'), + '#options' => $options, + '#description' => $this->t('The field that should be used as a source for the machine name element. This field needs to be displayed in the entity form before the @field_label field.', ['@field_label' => $this->fieldDefinition->getLabel()]), + ]; + $element['disable_on_edit'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Disable after initial creation'), + '#default_value' => $this->getSetting('disable_on_edit'), + '#description' => $this->t('Disable the machine name after the content has been saved for the first time.'), + ]; + $element['standalone'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Standalone'), + '#default_value' => $this->getSetting('standalone'), + '#description' => $this->t('Keep the machine name live preview as its own form element, separated from the source field.'), + ]; + $element['replace_pattern'] = [ + '#type' => 'textfield', + '#title' => $this->t('Replace pattern'), + '#default_value' => $this->getSetting('replace_pattern'), + '#description' => $this->t('A regular expression (without delimiters) matching disallowed characters in the machine name.'), + '#size' => 30, + ]; + $element['replace'] = [ + '#type' => 'textfield', + '#title' => $this->t('Replace character'), + '#default_value' => $this->getSetting('replace'), + '#description' => $this->t("A character to replace disallowed characters in the machine name. When using a different character than '_', Replace pattern needs to be set accordingly."), + '#size' => 1, + '#maxlength' => 1, + ]; + $element['size'] = [ + '#type' => 'number', + '#title' => $this->t('Size of machine name field'), + '#default_value' => $this->getSetting('size'), + '#required' => TRUE, + '#min' => 1, + ]; + + return $element; + } + + /** + * {@inheritdoc} + */ + public function settingsSummary() { + $summary = []; + + $field_definitions = $this->entityFieldManager->getFieldDefinitions($this->fieldDefinition->getTargetEntityTypeId(), $this->fieldDefinition->getTargetBundle()); + if (!empty($this->getSetting('source_field')) && isset($field_definitions[$this->getSetting('source_field')])) { + $summary[] = $this->t('Source field: @source_field', ['@source_field' => $field_definitions[$this->getSetting('source_field')]->getLabel()]); + $summary[] = $this->t('Disable on edit: @disable_on_edit', ['@disable_on_edit' => $this->getSetting('disable_on_edit') ? $this->t('Yes') : $this->t('No')]); + $summary[] = $this->t('Standalone: @standalone', ['@standalone' => $this->getSetting('standalone') ? $this->t('Yes') : $this->t('No')]); + $summary[] = $this->t('Replace pattern: @replace_pattern', ['@replace_pattern' => $this->getSetting('replace_pattern')]); + $summary[] = $this->t('Replace character: @replace', ['@replace' => $this->getSetting('replace')]); + $summary[] = $this->t('Machine name field size: @size', ['@size' => $this->getSetting('size')]); + } + else { + $summary[] = $this->t('Missing configuration.'); + } + + return $summary; + } + + /** + * {@inheritdoc} + */ + public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) { + $element_info = $this->elementInfo->getInfo('machine_name'); + $element['value'] = $element + [ + '#type' => 'machine_name', + '#default_value' => isset($items[$delta]->value) ? $items[$delta]->value : NULL, + '#maxlength' => $this->getFieldSetting('max_length'), + '#entity_type_id' => $this->fieldDefinition->getTargetEntityTypeId(), + '#field_name' => $this->fieldDefinition->getName(), + '#source_field' => $this->getSetting('source_field'), + '#process' => array_merge([[get_class($this), 'processMachineNameSource']], $element_info['#process']), + '#machine_name' => [ + // We don't need the default form-level validation because we enforce + // the 'UniqueField' constraint on the field that uses this widget. + 'exists' => [$this, 'existsCallback'], + 'label' => $this->fieldDefinition->getLabel(), + 'standalone' => $this->getSetting('standalone'), + 'replace_pattern' => $this->getSetting('replace_pattern'), + 'replace' => $this->getSetting('replace'), + ], + '#disabled' => $this->getSetting('disable_on_edit') && !$items->getEntity()->isNew(), + '#size' => $this->getSetting('size'), + ]; + + return $element; + } + + /** + * The machine_name exists callback. + * + * @return false + * We always return false because of usage of 'UniqueField' constraint. + */ + public function existsCallback() { + return FALSE; + } + + /** + * Form API callback: Sets the 'source' property of a machine_name element. + * + * This method is assigned as a #process callback in formElement() method. + */ + public static function processMachineNameSource($element, FormStateInterface $form_state, $form) { + $source_field_state = static::getWidgetState($element['#field_parents'], $element['#source_field'], $form_state); + + // Hide the field widget if the source field is not configured properly or + // if it doesn't exist in the form. + if (empty($element['#source_field']) || empty($source_field_state['array_parents'])) { + $element['#access'] = FALSE; + } + else { + $source_field_element = NestedArray::getValue($form_state->getCompleteForm(), $source_field_state['array_parents']); + $element['#machine_name']['source'] = $source_field_element[$element['#delta']]['value']['#array_parents']; + } + + return $element; + } + + /** + * {@inheritdoc} + */ + public static function isApplicable(FieldDefinitionInterface $field_definition) { + // This widget is only available to fields that have a 'UniqueField' + // constraint. + $constraints = $field_definition->getConstraints(); + return array_key_exists('UniqueField', $constraints); + } + +} diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldValueValidator.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldValueValidator.php index e965310861a..24c579ceb6e 100644 --- a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldValueValidator.php +++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/UniqueFieldValueValidator.php @@ -26,11 +26,10 @@ public function validate($items, Constraint $constraint) { $query = \Drupal::entityQuery($entity_type_id) ->accessCheck(FALSE); - $entity_id = $entity->id(); - // Using isset() instead of !empty() as 0 and '0' are valid ID values for - // entity types using string IDs. - if (isset($entity_id)) { - $query->condition($id_key, $entity_id, '<>'); + // If the entity already exists in the storage, ensure that we don't compare + // the field value with the pre-existing one. + if (!$entity->isNew()) { + $query->condition($id_key, $entity->id(), '<>'); } $value_taken = (bool) $query 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 9e39a5a01d2..f533f419dad 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 @@ -49,7 +49,13 @@ 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) + ->addConstraint('UniqueField', []) + ->setDisplayConfigurable('form', TRUE); + + // Make the label field configurable in the UI. + $fields['name']->setDisplayConfigurable('form', TRUE); + return $fields; } diff --git a/core/modules/workspaces/src/Entity/Workspace.php b/core/modules/workspaces/src/Entity/Workspace.php index fdde2399a71..62c708b4fcc 100644 --- a/core/modules/workspaces/src/Entity/Workspace.php +++ b/core/modules/workspaces/src/Entity/Workspace.php @@ -81,16 +81,26 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ->setDescription(new TranslatableMarkup('The workspace ID.')) ->setSetting('max_length', 128) ->setRequired(TRUE) - ->addConstraint('UniqueField') - ->addConstraint('DeletedWorkspace') - ->addPropertyConstraints('value', ['Regex' => ['pattern' => '/^[a-z0-9_]+$/']]); + ->addConstraint('UniqueField', []) + ->addConstraint('DeletedWorkspace', []) + ->addPropertyConstraints('value', ['Regex' => ['pattern' => '/^[a-z0-9_]+$/']]) + ->setDisplayOptions('form', [ + 'type' => 'machine_name', + 'weight' => -5, + 'settings' => [ + 'source_field' => 'label', + ], + ]); $fields['label'] = BaseFieldDefinition::create('string') ->setLabel(new TranslatableMarkup('Workspace name')) - ->setDescription(new TranslatableMarkup('The workspace name.')) ->setRevisionable(TRUE) ->setSetting('max_length', 128) - ->setRequired(TRUE); + ->setRequired(TRUE) + ->setDisplayOptions('form', [ + 'type' => 'string_textfield', + 'weight' => -10, + ]); $fields['uid'] ->setLabel(new TranslatableMarkup('Owner')) diff --git a/core/modules/workspaces/src/Form/WorkspaceForm.php b/core/modules/workspaces/src/Form/WorkspaceForm.php index 13a5b60ffd7..de8d6d98360 100644 --- a/core/modules/workspaces/src/Form/WorkspaceForm.php +++ b/core/modules/workspaces/src/Form/WorkspaceForm.php @@ -4,7 +4,6 @@ use Drupal\Component\Datetime\TimeInterface; use Drupal\Core\Entity\ContentEntityForm; -use Drupal\Core\Entity\EntityConstraintViolationListInterface; use Drupal\Core\Entity\EntityRepositoryInterface; use Drupal\Core\Entity\EntityTypeBundleInfoInterface; use Drupal\Core\Form\FormStateInterface; @@ -69,57 +68,10 @@ public function form(array $form, FormStateInterface $form_state) { if ($this->operation == 'edit') { $form['#title'] = $this->t('Edit workspace %label', ['%label' => $workspace->label()]); } - $form['label'] = [ - '#type' => 'textfield', - '#title' => $this->t('Label'), - '#maxlength' => 255, - '#default_value' => $workspace->label(), - '#required' => TRUE, - ]; - - $form['id'] = [ - '#type' => 'machine_name', - '#title' => $this->t('Workspace ID'), - '#maxlength' => 255, - '#default_value' => $workspace->id(), - '#disabled' => !$workspace->isNew(), - '#machine_name' => [ - 'exists' => '\Drupal\workspaces\Entity\Workspace::load', - ], - '#element_validate' => [], - ]; return parent::form($form, $form_state); } - /** - * {@inheritdoc} - */ - protected function getEditedFieldNames(FormStateInterface $form_state) { - return array_merge([ - 'label', - 'id', - ], parent::getEditedFieldNames($form_state)); - } - - /** - * {@inheritdoc} - */ - protected function flagViolations(EntityConstraintViolationListInterface $violations, array $form, FormStateInterface $form_state) { - // Manually flag violations of fields not handled by the form display. This - // is necessary as entity form displays only flag violations for fields - // contained in the display. - $field_names = [ - 'label', - 'id', - ]; - foreach ($violations->getByFields($field_names) as $violation) { - [$field_name] = explode('.', $violation->getPropertyPath(), 2); - $form_state->setErrorByName($field_name, $violation->getMessage()); - } - parent::flagViolations($violations, $form, $form_state); - } - /** * {@inheritdoc} */ diff --git a/core/modules/workspaces/tests/src/Functional/WorkspacePermissionsTest.php b/core/modules/workspaces/tests/src/Functional/WorkspacePermissionsTest.php index 7d0a3175a5d..af37ab48810 100644 --- a/core/modules/workspaces/tests/src/Functional/WorkspacePermissionsTest.php +++ b/core/modules/workspaces/tests/src/Functional/WorkspacePermissionsTest.php @@ -74,8 +74,7 @@ public function testEditOwnWorkspace() { $this->assertSession()->statusCodeEquals(200); $page = $this->getSession()->getPage(); - $page->fillField('label', 'Bears again'); - $page->fillField('id', 'bears'); + $page->fillField('label[0][value]', 'Bears again'); $page->findButton('Save')->click(); $page->hasContent('Bears again (bears)'); @@ -118,8 +117,7 @@ public function testEditAnyWorkspace() { $this->assertSession()->statusCodeEquals(200); $page = $this->getSession()->getPage(); - $page->fillField('label', 'Bears again'); - $page->fillField('id', 'bears'); + $page->fillField('label[0][value]', 'Bears again'); $page->findButton('Save')->click(); $page->hasContent('Bears again (bears)'); diff --git a/core/modules/workspaces/tests/src/Functional/WorkspaceTest.php b/core/modules/workspaces/tests/src/Functional/WorkspaceTest.php index 5c04a00ba2b..f775f0ecddd 100644 --- a/core/modules/workspaces/tests/src/Functional/WorkspaceTest.php +++ b/core/modules/workspaces/tests/src/Functional/WorkspaceTest.php @@ -73,21 +73,22 @@ protected function setUp(): void { /** * Tests creating a workspace with special characters. */ - public function testSpecialCharacters() { + public function testWorkspaceCreate() { $this->drupalLogin($this->editor1); + $page = $this->getSession()->getPage(); - // Test a valid workspace name. - $this->createWorkspaceThroughUi('Workspace 1', 'a0_$()+-/'); + // Test a valid workspace ID. + $workspace = $this->createWorkspaceThroughUi('Workspace 1', 'workspace_1'); + $this->assertEquals('workspace_1', $workspace->id()); - // Test and invalid workspace name. - $this->drupalGet('/admin/config/workflow/workspaces/add'); - $this->assertSession()->statusCodeEquals(200); + // Test an invalid workspace ID. + $workspace = $this->createWorkspaceThroughUi('Workspace 2', 'workspace A@-'); + $this->assertNull($workspace); + $this->assertTrue($page->hasContent('The machine-readable name must contain only lowercase letters, numbers, and underscores.')); - $page = $this->getSession()->getPage(); - $page->fillField('label', 'workspace2'); - $page->fillField('id', 'A!"£%^&*{}#~@?'); - $page->findButton('Save')->click(); - $page->hasContent("This value is not valid"); + // Test a duplicate workspace ID. + $this->createWorkspaceThroughUi('Workspace 1 again', 'workspace_1'); + $this->assertTrue($page->hasContent('A workspace with Workspace ID workspace_1 already exists.')); } /** @@ -96,11 +97,7 @@ public function testSpecialCharacters() { public function testWorkspaceToolbar() { $this->drupalLogin($this->editor1); - $this->drupalGet('/admin/config/workflow/workspaces/add'); - $this->submitForm([ - 'id' => 'test_workspace', - 'label' => 'Test workspace', - ], 'Save'); + $this->createWorkspaceThroughUi('Test workspace', 'test_workspace'); // Activate the test workspace. $this->drupalGet('/admin/config/workflow/workspaces/manage/test_workspace/activate'); @@ -113,7 +110,7 @@ public function testWorkspaceToolbar() { // Change the workspace label. $this->drupalGet('/admin/config/workflow/workspaces/manage/test_workspace/edit'); - $this->submitForm(['label' => 'New name'], 'Save'); + $this->submitForm(['label[0][value]' => 'New name'], 'Save'); $this->drupalGet(''); $page = $this->getSession()->getPage(); @@ -127,11 +124,7 @@ public function testWorkspaceToolbar() { public function testWorkspaceOwner() { $this->drupalLogin($this->editor1); - $this->drupalGet('/admin/config/workflow/workspaces/add'); - $this->submitForm([ - 'id' => 'test_workspace', - 'label' => 'Test workspace', - ], 'Save'); + $this->createWorkspaceThroughUi('Test workspace', 'test_workspace'); $storage = \Drupal::entityTypeManager()->getStorage('workspace'); $test_workspace = $storage->load('test_workspace'); diff --git a/core/modules/workspaces/tests/src/Functional/WorkspaceTestUtilities.php b/core/modules/workspaces/tests/src/Functional/WorkspaceTestUtilities.php index 1faa3ee12a8..e48f984039f 100644 --- a/core/modules/workspaces/tests/src/Functional/WorkspaceTestUtilities.php +++ b/core/modules/workspaces/tests/src/Functional/WorkspaceTestUtilities.php @@ -60,13 +60,11 @@ protected function getOneEntityByLabel($type, $label) { protected function createWorkspaceThroughUi($label, $id, $parent = '_none') { $this->drupalGet('/admin/config/workflow/workspaces/add'); $this->submitForm([ - 'id' => $id, - 'label' => $label, + 'id[0][value]' => $id, + 'label[0][value]' => $label, 'parent' => $parent, ], 'Save'); - $this->getSession()->getPage()->hasContent("$label ($id)"); - return Workspace::load($id); } diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Core/Field/Widget/MachineNameWidgetTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Core/Field/Widget/MachineNameWidgetTest.php new file mode 100644 index 00000000000..91b930213c8 --- /dev/null +++ b/core/tests/Drupal/FunctionalJavascriptTests/Core/Field/Widget/MachineNameWidgetTest.php @@ -0,0 +1,233 @@ +drupalLogin($this->drupalCreateUser(['access content', 'view test entity', 'administer entity_test content', 'administer entity_test_string_id form display'])); + } + + /** + * Tests the machine name field widget. + */ + 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. + \Drupal::service('entity_display.repository') + ->getFormDisplay('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_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. + \Drupal::service('entity_display.repository') + ->getFormDisplay('entity_test_string_id', 'entity_test_string_id', 'default') + ->setComponent('id', [ + 'type' => 'machine_name', + 'weight' => 5, + ]) + ->save(); + + // Check that the widget displays an "error" summary when it has missing or + // broken settings. + $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[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('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_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. + \Drupal::service('entity_display.repository') + ->getFormDisplay('entity_test_string_id', 'entity_test_string_id', 'default') + ->setComponent('name', [ + 'type' => 'string_textfield', + 'weight' => 10, + ]) + ->save(); + + // 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'); + + // Check that the newly added 'name' field can not be selected as a source + // field because it has a higher weight in the form display than the field + // using the machine name widget. + $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'); + + // Configure the source field to have a lower weight than the test field, + // which will make it appear as an option for the 'Source field' setting. + \Drupal::service('entity_display.repository') + ->getFormDisplay('entity_test_string_id', 'entity_test_string_id', 'default') + ->setComponent('name', [ + 'type' => 'string_textfield', + 'weight' => 4, + ]) + ->save(); + + // Configure the test field to use the newly enabled 'source' field as the + // machine name source. + $this->drupalGet('/entity_test_string_id/structure/entity_test_string_id/form-display'); + $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: Name'); + $assert_session->pageTextContains('Replace pattern: [^a-z0-9_]+'); + $assert_session->pageTextContains('Replace character: _'); + + $this->submitForm([], 'Save'); + $assert_session->pageTextContains('Your settings have been saved.'); + + $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($id); + $this->assertNotEmpty($id_machine_name_value, 'Test field with the machine name widget has been initialized.'); + + $test_values = [ + 'input' => 'Test value !0-9@', + 'expected' => 'test_value_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'); + + // Load the entity and check that machine name value that was saved is + // correct. + $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. + \Drupal::service('entity_display.repository') + ->getFormDisplay('entity_test_string_id', 'entity_test_string_id', 'default') + ->setComponent('id', [ + 'type' => 'machine_name', + 'weight' => 5, + 'settings' => [ + 'source_field' => 'name', + 'replace_pattern' => '[^a-z0-9-]+', + 'replace' => '-', + ], + ]) + ->save(); + + $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'); + + // Load the entity and check that machine name value that was saved is + // correct. + $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('A test entity with string_id with id test-value2-0-9- already exists.'); + } + +} diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Core/MachineNameTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Core/MachineNameTest.php index 438021d5e92..30bfa3213be 100644 --- a/core/tests/Drupal/FunctionalJavascriptTests/Core/MachineNameTest.php +++ b/core/tests/Drupal/FunctionalJavascriptTests/Core/MachineNameTest.php @@ -5,9 +5,9 @@ use Drupal\FunctionalJavascriptTests\WebDriverTestBase; /** - * Tests for the machine name field. + * Tests for the machine name form element. * - * @group field + * @group Form */ class MachineNameTest extends WebDriverTestBase {