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 {