From 3d8a748c0c46c5fb75f597299c9538bc04f22f00 Mon Sep 17 00:00:00 2001
From: Sascha Grossenbacher <5019-berdir@users.noreply.drupalcode.org>
Date: Tue, 6 Aug 2024 19:24:49 +0000
Subject: [PATCH] Issue #3027525 by Berdir, marcoscano, robin.ingelbrecht,
 nkamala, jsst, Maico de Jong, NikolaAt, kala4ek, miro_dietiker: Show the
 paragraph and field label of fields that fail validation

---
 .../Field/FieldWidget/ParagraphsWidget.php    | 89 ++++++++++++++++++-
 .../ParagraphsAdministrationTest.php          |  2 +-
 2 files changed, 88 insertions(+), 3 deletions(-)

diff --git a/src/Plugin/Field/FieldWidget/ParagraphsWidget.php b/src/Plugin/Field/FieldWidget/ParagraphsWidget.php
index 2e061f76..2333b040 100644
--- a/src/Plugin/Field/FieldWidget/ParagraphsWidget.php
+++ b/src/Plugin/Field/FieldWidget/ParagraphsWidget.php
@@ -6,6 +6,7 @@ use Drupal\Component\Utility\NestedArray;
 use Drupal\Component\Utility\Html;
 use Drupal\Core\Entity\ContentEntityInterface;
 use Drupal\Core\Entity\Entity\EntityFormDisplay;
+use Drupal\Core\Entity\EntityFieldManagerInterface;
 use Drupal\Core\Entity\FieldableEntityInterface;
 use Drupal\Core\Field\FieldDefinitionInterface;
 use Drupal\Core\Field\FieldFilteredMarkup;
@@ -18,8 +19,10 @@ use Drupal\Core\Render\Element;
 use Drupal\Core\TypedData\TranslationStatusInterface;
 use Drupal\field_group\FormatterHelper;
 use Drupal\paragraphs\Entity\Paragraph;
+use Drupal\paragraphs\Entity\ParagraphsType;
 use Drupal\paragraphs\ParagraphInterface;
 use Drupal\paragraphs\Plugin\EntityReferenceSelection\ParagraphSelection;
+use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\Validator\ConstraintViolationInterface;
 use Symfony\Component\Validator\ConstraintViolationListInterface;
 
@@ -94,6 +97,13 @@ class ParagraphsWidget extends WidgetBase {
    */
   protected $accessOptions = NULL;
 
+  /**
+   * The entity field manager service.
+   *
+   * @var \Drupal\Core\Entity\EntityFieldManagerInterface
+   */
+  protected $entityFieldManager;
+
   /**
    * Constructs a ParagraphsWidget object.
    *
@@ -107,17 +117,35 @@ class ParagraphsWidget extends WidgetBase {
    *   The widget settings.
    * @param array $third_party_settings
    *   Any third party settings.
+   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
+   *   The entity field manager service.
    */
-  public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings) {
+  public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, EntityFieldManagerInterface $entity_field_manager) {
     // Modify settings that were set before https://www.drupal.org/node/2896115.
     if(isset($settings['edit_mode']) && $settings['edit_mode'] === 'preview') {
       $settings['edit_mode'] = 'closed';
       $settings['closed_mode'] = 'preview';
     }
 
+    $this->entityFieldManager = $entity_field_manager;
+
     parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings);
   }
 
+  /**
+   * {@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'),
+    );
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -2653,7 +2681,7 @@ class ParagraphsWidget extends WidgetBase {
                 continue;
               }
 
-              $form_state->setError($element[$item['_original_delta']], $this->t('Validation error on collapsed paragraph @path: @message', ['@path' => $violation->getPropertyPath(), '@message' => $violation->getMessage()]));
+              $this->massageCollapsedParagraphsErrorMessages($form, $form_state, $element, $item, $violation);
             }
           }
         }
@@ -3107,4 +3135,61 @@ class ParagraphsWidget extends WidgetBase {
     return FALSE;
   }
 
+  /**
+   * Improve validation error messages by including additional labels.
+   *
+   * This will convert an error message of type:
+   *   "Text field is required."
+   * into something like:
+   *   "Error in field Content #1 (Hero): Text field is required."
+   * where "Content" is the top-level paragraph field label, "Hero" is the
+   * paragraph type label, and "1" is the position of the paragraph in the
+   * field (delta + 1).
+   *
+   * @param array $form
+   *   The form array.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The form state object.
+   * @param array $element
+   *   Element with error.
+   * @param array $item
+   *   Sequence of the item.
+   * @param \Symfony\Component\Validator\ConstraintViolationInterface $violation
+   *   Violation of the field.
+   */
+  protected function massageCollapsedParagraphsErrorMessages(array $form, FormStateInterface $form_state, $element, $item, $violation) {
+    $field_name = $this->fieldDefinition->getName();
+    $field_label = $form[$field_name]['widget']['#title'] ?? FALSE;
+    $delta = $item['_original_delta'];
+    $paragraph_type = $form[$field_name]['widget'][$delta]['#paragraph_type'] ?? FALSE;
+    if ($field_label && $paragraph_type) {
+      $paragraph_type_label = ParagraphsType::load($paragraph_type)->label();
+      $property_path = $violation->getPropertyPath();
+      $property_path = explode('.', $property_path)[0];
+
+      $fields = $this->entityFieldManager->getFieldDefinitions('paragraph', $paragraph_type);
+      if (isset($fields[$property_path])) {
+        $new_message = $this->t('Error in field %field #@position (@bundle), %subfield : @message', [
+          '%field' => $field_label,
+          '@position' => $delta + 1,
+          '@bundle' => $paragraph_type_label,
+          '%subfield' => $fields[$property_path]->getLabel(),
+          '@message' => $violation->getMessage(),
+        ]);
+      }
+      else {
+        $new_message = $this->t('Error in field %field #@position (@bundle): @message', [
+          '%field' => $field_label,
+          '@position' => $delta + 1,
+          '@bundle' => $paragraph_type_label,
+          '@message' => $violation->getMessage(),
+        ]);
+      }
+
+      $form_state->setError($element[$item['_original_delta']], $new_message);
+    } else {
+      $form_state->setError($element[$item['_original_delta']], $this->t('Validation error on collapsed paragraph @path: @message', ['@path' => $violation->getPropertyPath(), '@message' => $violation->getMessage()]));
+    }
+  }
+
 }
diff --git a/tests/src/Functional/WidgetStable/ParagraphsAdministrationTest.php b/tests/src/Functional/WidgetStable/ParagraphsAdministrationTest.php
index eb278025..139b3732 100644
--- a/tests/src/Functional/WidgetStable/ParagraphsAdministrationTest.php
+++ b/tests/src/Functional/WidgetStable/ParagraphsAdministrationTest.php
@@ -489,7 +489,7 @@ class ParagraphsAdministrationTest extends ParagraphsTestBase {
     $this->assertSession()->pageTextNotContains('The referenced entity (node: ' . $node->id() . ') does not exist.');
     $this->assertSession()->fieldNotExists('field_paragraphs[1][subform][field_entity_reference][0][target_id]');
     $this->submitForm([], 'Save');
-    $this->assertSession()->pageTextContains('Validation error on collapsed paragraph field_entity_reference.0.target_id: The referenced entity (node: ' . $node->id() . ') does not exist.');
+    $this->assertSession()->pageTextContains('Error in field field_paragraphs #1 (node_test), Entity reference : The referenced entity (node: ' . $node->id() . ') does not exist.');
 
     // Attempt to edit the Paragraph.
     $this->submitForm([], 'field_paragraphs_0_edit');
-- 
GitLab