diff --git a/core/lib/Drupal/Core/Entity/ContentEntityForm.php b/core/lib/Drupal/Core/Entity/ContentEntityForm.php index 4c741aef337..92bbbae42f9 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityForm.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityForm.php @@ -188,10 +188,29 @@ class ContentEntityForm extends EntityForm implements ContentEntityFormInterface $violations = $entity->validate(); - // Remove violations of inaccessible fields and not edited fields. - $violations - ->filterByFieldAccess($this->currentUser()) - ->filterByFields(array_diff(array_keys($entity->getFieldDefinitions()), $this->getEditedFieldNames($form_state))); + // Remove violations of inaccessible fields. + $violations->filterByFieldAccess($this->currentUser()); + + // In case a field-level submit button is clicked, for example the 'Add + // another item' button for multi-value fields or the 'Upload' button for a + // File or an Image field, make sure that we only keep violations for that + // specific field. + $edited_fields = []; + if ($limit_validation_errors = $form_state->getLimitValidationErrors()) { + foreach ($limit_validation_errors as $section) { + $field_name = reset($section); + if ($entity->hasField($field_name)) { + $edited_fields[] = $field_name; + } + } + $edited_fields = array_unique($edited_fields); + } + else { + $edited_fields = $this->getEditedFieldNames($form_state); + } + + // Remove violations for fields that are not edited. + $violations->filterByFields(array_diff(array_keys($entity->getFieldDefinitions()), $edited_fields)); $this->flagViolations($violations, $form, $form_state); diff --git a/core/tests/Drupal/FunctionalTests/Entity/ContentEntityFormFieldValidationFilteringTest.php b/core/tests/Drupal/FunctionalTests/Entity/ContentEntityFormFieldValidationFilteringTest.php new file mode 100644 index 00000000000..0f17e6425b6 --- /dev/null +++ b/core/tests/Drupal/FunctionalTests/Entity/ContentEntityFormFieldValidationFilteringTest.php @@ -0,0 +1,164 @@ +drupalCreateUser(['administer entity_test content']); + $this->drupalLogin($web_user); + + // Create two fields of field type "test_field", one with single cardinality + // and one with unlimited cardinality on the entity type "entity_test". It + // is important to use this field type because its default widget has a + // custom \Drupal\Core\Field\WidgetInterface::errorElement() implementation. + $this->entityTypeId = 'entity_test'; + $this->fieldNameSingle = 'test_single'; + $this->fieldNameMultiple = 'test_multiple'; + $this->fieldNameFile = 'test_file'; + + FieldStorageConfig::create([ + 'field_name' => $this->fieldNameSingle, + 'entity_type' => $this->entityTypeId, + 'type' => 'test_field', + 'cardinality' => 1, + ])->save(); + FieldConfig::create([ + 'entity_type' => $this->entityTypeId, + 'field_name' => $this->fieldNameSingle, + 'bundle' => $this->entityTypeId, + 'label' => 'Test single', + 'required' => TRUE, + 'translatable' => FALSE, + ])->save(); + + FieldStorageConfig::create([ + 'field_name' => $this->fieldNameMultiple, + 'entity_type' => $this->entityTypeId, + 'type' => 'test_field', + 'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, + ])->save(); + FieldConfig::create([ + 'entity_type' => $this->entityTypeId, + 'field_name' => $this->fieldNameMultiple, + 'bundle' => $this->entityTypeId, + 'label' => 'Test multiple', + 'translatable' => FALSE, + ])->save(); + + // Also create a file field to test its '#limit_validation_errors' + // implementation. + FieldStorageConfig::create([ + 'field_name' => $this->fieldNameFile, + 'entity_type' => $this->entityTypeId, + 'type' => 'file', + 'cardinality' => 1, + ])->save(); + FieldConfig::create([ + 'entity_type' => $this->entityTypeId, + 'field_name' => $this->fieldNameFile, + 'bundle' => $this->entityTypeId, + 'label' => 'Test file', + 'translatable' => FALSE, + ])->save(); + + + entity_get_form_display($this->entityTypeId, $this->entityTypeId, 'default') + ->setComponent($this->fieldNameSingle, ['type' => 'test_field_widget']) + ->setComponent($this->fieldNameMultiple, ['type' => 'test_field_widget']) + ->setComponent($this->fieldNameFile, ['type' => 'file_generic']) + ->save(); + } + + /** + * Tests field widgets with #limit_validation_errors. + */ + public function testFieldWidgetsWithLimitedValidationErrors() { + $assert_session = $this->assertSession(); + $this->drupalGet($this->entityTypeId . '/add'); + + // The 'Test multiple' field is the only multi-valued field in the form, so + // try to add a new item for it. This tests the '#limit_validation_errors' + // property set by \Drupal\Core\Field\WidgetBase::formMultipleElements(). + $assert_session->elementsCount('css', 'div#edit-test-multiple-wrapper div.form-type-textfield input', 1); + $this->drupalPostForm(NULL, [], 'Add another item'); + $assert_session->elementsCount('css', 'div#edit-test-multiple-wrapper div.form-type-textfield input', 2); + + // Now try to upload a file. This tests the '#limit_validation_errors' + // property set by + // \Drupal\file\Plugin\Field\FieldWidget\FileWidget::process(). + $text_file = current($this->getTestFiles('text')); + $edit = [ + 'files[test_file_0]' => drupal_realpath($text_file->uri) + ]; + $assert_session->elementNotExists('css', 'input#edit-test-file-0-remove-button'); + $this->drupalPostForm(NULL, $edit, 'Upload'); + $assert_session->elementExists('css', 'input#edit-test-file-0-remove-button'); + + // Make the 'Test multiple' field required and check that adding another + // item throws a validation error. + $field_config = FieldConfig::loadByName($this->entityTypeId, $this->entityTypeId, $this->fieldNameMultiple); + $field_config->setRequired(TRUE); + $field_config->save(); + + $this->drupalPostForm($this->entityTypeId . '/add', [], 'Add another item'); + $assert_session->pageTextContains('Test multiple (value 1) field is required.'); + + // Check that saving the form without entering any value for the required + // field still throws the proper validation errors. + $this->drupalPostForm(NULL, [], 'Save'); + $assert_session->pageTextContains('Test single field is required.'); + $assert_session->pageTextContains('Test multiple (value 1) field is required.'); + } + +}