Commit 7596d708 authored by Sascha Grossenbacher's avatar Sascha Grossenbacher Committed by Sascha Grossenbacher
Browse files

Issue #2901390 by mxh, Webbeh, sgurlt, Berdir, Chris Burge, oknate, lakshmi_a,...

Issue #2901390 by mxh, Webbeh, sgurlt, Berdir, Chris Burge, oknate, lakshmi_a, FiNeX, amol.palhade17, hanoii, sarguna raj M:  Integrity constraint violation: 1048 Column 'langcode' cannot be null
parent b144b071
Loading
Loading
Loading
Loading
+26 −8
Original line number Diff line number Diff line
@@ -298,8 +298,7 @@ class InlineParagraphsWidget extends WidgetBase {
    if ($paragraphs_entity) {
      // Detect if we are translating.
      $this->initIsTranslating($form_state, $host);
      $langcode = $form_state->get('langcode');

      $langcode = $this->getCurrentLangcode($form_state, $items);
      if (!$this->isTranslating) {
        // Set the langcode if we are not translating.
        $langcode_key = $paragraphs_entity->getEntityType()->getKey('langcode');
@@ -929,6 +928,14 @@ class InlineParagraphsWidget extends WidgetBase {

    $host = $items->getEntity();
    $this->initIsTranslating($form_state, $host);
    if (!$form_state->has('langcode')) {
      // Entity forms usually have the langcode set, but it's not guaranteed.
      // Any form object can embed entities with their field widgets.
      // At this point, without knowing the langcode from the form state,
      // it's not certain which language is chosen. Remember the host entity,
      // to get the langcode at a stage when the chosen value is more certain.
      $elements['#host'] = $host;
    }

    if (($this->realItemCount < $cardinality || $cardinality == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) && !$form_state->isProgrammed() && !$this->isTranslating) {
      $elements['add_more'] = $this->buildAddActions();
@@ -1127,11 +1134,15 @@ class InlineParagraphsWidget extends WidgetBase {
   * is only accessibly through the form state.
   *
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   * @param \Drupal\Core\Field\FieldItemListInterface $items
   *   The field item list.
   *
   * @return string
   *   The language code.
   */
  protected function getCurrentLangcode(FormStateInterface $form_state, FieldItemListInterface $items) {
    return $form_state->get('langcode') ?: $items->getEntity()->language()->getId();
    return $form_state->has('langcode') ? $form_state->get('langcode') : $items->getEntity()->language()->getId();
  }

  /**
@@ -1318,14 +1329,20 @@ class InlineParagraphsWidget extends WidgetBase {
        // A content entity form saves without any rebuild. It needs to set the
        // language to update it in case of language change.
        $langcode_key = $paragraphs_entity->getEntityType()->getKey('langcode');
        if ($paragraphs_entity->get($langcode_key)->value != $form_state->get('langcode')) {
        $langcode = $form_state->get('langcode');
        if (!isset($langcode) && isset($element['#host'])) {
          // Use the host entity as a last resort to determine the langcode.
          // @see self::formMultipleElements
          $langcode = $element['#host']->language()->getId();
        }
        if ($paragraphs_entity->get($langcode_key)->value != $langcode) {
          // If a translation in the given language already exists, switch to
          // that. If there is none yet, update the language.
          if ($paragraphs_entity->hasTranslation($form_state->get('langcode'))) {
            $paragraphs_entity = $paragraphs_entity->getTranslation($form_state->get('langcode'));
          if ($paragraphs_entity->hasTranslation($langcode)) {
            $paragraphs_entity = $paragraphs_entity->getTranslation($langcode);
          }
          else {
            $paragraphs_entity->set($langcode_key, $form_state->get('langcode'));
            $paragraphs_entity->set($langcode_key, $langcode);
          }
        }

@@ -1380,7 +1397,8 @@ class InlineParagraphsWidget extends WidgetBase {
      // Adding a language through the ContentTranslationController.
      $this->isTranslating = TRUE;
    }
    if ($host->hasTranslation($form_state->get('langcode')) && $host->getTranslation($form_state->get('langcode'))->get($default_langcode_key)->value == 0) {
    $langcode = $form_state->get('langcode');
    if (isset($langcode) && $host->hasTranslation($langcode) && $host->getTranslation($langcode)->get($default_langcode_key)->value == 0) {
      // Editing a translation.
      $this->isTranslating = TRUE;
    }
+40 −8
Original line number Diff line number Diff line
@@ -419,8 +419,7 @@ class ParagraphsWidget extends WidgetBase {
    if ($paragraphs_entity) {
      // Detect if we are translating.
      $this->initIsTranslating($form_state, $host);
      $langcode = $form_state->get('langcode');

      $langcode = $this->getCurrentLangcode($form_state, $items);
      if (!$this->isTranslating) {
        // Set the langcode if we are not translating.
        $langcode_key = $paragraphs_entity->getEntityType()->getKey('langcode');
@@ -1170,6 +1169,14 @@ class ParagraphsWidget extends WidgetBase {

    $host = $items->getEntity();
    $this->initIsTranslating($form_state, $host);
    if (!$form_state->has('langcode')) {
      // Entity forms usually have the langcode set, but it's not guaranteed.
      // Any form object can embed entities with their field widgets.
      // At this point, without knowing the langcode from the form state,
      // it's not certain which language is chosen. Remember the host entity,
      // to get the langcode at a stage when the chosen value is more certain.
      $elements['#host'] = $host;
    }

    $header_actions = $this->buildHeaderActions($field_state, $form_state);
    if ($header_actions) {
@@ -1700,6 +1707,25 @@ class ParagraphsWidget extends WidgetBase {
    return $add_more_elements;
  }

  /**
   * Gets current language code from the form state or item.
   *
   * Since the paragraph field is not set as translatable, the item language
   * code is set to the source language. The intended translation language
   * is only accessibly through the form state.
   *
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   * @param \Drupal\Core\Field\FieldItemListInterface $items
   *   The field item list.
   *
   * @return string
   *   The language code.
   */
  protected function getCurrentLangcode(FormStateInterface $form_state, FieldItemListInterface $items) {
    return $form_state->has('langcode') ? $form_state->get('langcode') : $items->getEntity()->language()->getId();
  }

  /**
   * {@inheritdoc}
   */
@@ -2241,7 +2267,7 @@ class ParagraphsWidget extends WidgetBase {
      return $values;
    }

    foreach ($values as $delta => &$item) {
    foreach ($values as &$item) {
      if (isset($widget_state['paragraphs'][$item['_original_delta']]['entity'])
        && $widget_state['paragraphs'][$item['_original_delta']]['mode'] != 'remove') {
        /** @var \Drupal\paragraphs\ParagraphInterface $paragraphs_entity */
@@ -2255,14 +2281,20 @@ class ParagraphsWidget extends WidgetBase {
        // A content entity form saves without any rebuild. It needs to set the
        // language to update it in case of language change.
        $langcode_key = $paragraphs_entity->getEntityType()->getKey('langcode');
        if ($paragraphs_entity->get($langcode_key)->value != $form_state->get('langcode')) {
        $langcode = $form_state->get('langcode');
        if (!isset($langcode) && isset($element['#host'])) {
          // Use the host entity as a last resort to determine the langcode.
          // @see self::formMultipleElements
          $langcode = $element['#host']->language()->getId();
        }
        if ($paragraphs_entity->get($langcode_key)->value != $langcode) {
          // If a translation in the given language already exists, switch to
          // that. If there is none yet, update the language.
          if ($paragraphs_entity->hasTranslation($form_state->get('langcode'))) {
            $paragraphs_entity = $paragraphs_entity->getTranslation($form_state->get('langcode'));
          if ($paragraphs_entity->hasTranslation($langcode)) {
            $paragraphs_entity = $paragraphs_entity->getTranslation($langcode);
          }
          else {
            $paragraphs_entity->set($langcode_key, $form_state->get('langcode'));
            $paragraphs_entity->set($langcode_key, $langcode);
          }
        }
        if (isset($item['behavior_plugins'])) {
@@ -2378,7 +2410,7 @@ class ParagraphsWidget extends WidgetBase {
      $this->isTranslating = TRUE;
    }
    $langcode = $form_state->get('langcode');
    if ($host->hasTranslation($langcode) && $host->getTranslation($langcode)->get($default_langcode_key)->value == 0) {
    if (isset($langcode) && $host->hasTranslation($langcode) && $host->getTranslation($langcode)->get($default_langcode_key)->value == 0) {
      // Editing a translation.
      $this->isTranslating = TRUE;
    }
+104 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\paragraphs_test\Form;

use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Form\FormInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\SubformStateInterface;

/**
 * A class to build a form that embeds a content entity form.
 *
 * The logic of form processing is based on Layout Builder's InlineBlock
 * form processing.
 */
class TestEmbeddedEntityForm implements FormInterface {

  /**
   * The entity of the embedded form.
   *
   * @var \Drupal\Core\Entity\ContentEntityInterface
   */
  protected $entity;

  /**
   * TestEmbeddedEntityForm constructor.
   *
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
   *   The entity of the embedded form.
   */
  public function __construct(ContentEntityInterface $entity) {
    $this->entity = $entity;
  }

  /**
   * Get the entity of this form object.
   *
   * @return \Drupal\Core\Entity\ContentEntityInterface
   *   The entity.
   */
  public function getEntity() {
    return $this->entity;
  }

  /**
   * {@inheritdoc}
   */
  public function getFormId() {
    return 'test_embedded_entity_form';
  }

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {
    // Based on the logic of Layout Builder's InlineBlock form processing,
    // the entity form is being inserted via process callback.
    return [
      'embedded_entity_form' => [
        '#type' => 'container',
        '#process' => [[static::class, 'processEmbeddedEntityForm']],
        '#entity' => $this->entity,
      ],
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function validateForm(array &$form, FormStateInterface $form_state) {}

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    $embedded_form = $form['embedded_entity_form'];
    $this->entity = $embedded_form['#entity'];

    $form_display = EntityFormDisplay::collectRenderDisplay($this->entity, 'default');
    $complete_form_state = $form_state instanceof SubformStateInterface ? $form_state->getCompleteFormState() : $form_state;
    $form_display->extractFormValues($this->entity, $embedded_form, $complete_form_state);
    $this->entity->save();
  }

  /**
   * Process callback to embed an entity form.
   *
   * @param array $element
   *   The containing element.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   *
   * @return array
   *   The containing element, with the entity form inserted.
   */
  public static function processEmbeddedEntityForm(array $element, FormStateInterface $form_state) {
    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
    $entity = $element['#entity'];
    EntityFormDisplay::collectRenderDisplay($entity, 'default')->buildForm($entity, $element, $form_state);
    return $element;
  }

}
+384 −0
Original line number Diff line number Diff line
<?php

namespace Drupal\Tests\paragraphs\Kernel;

use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Form\FormState;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
use Drupal\language\Entity\ContentLanguageSettings;
use Drupal\paragraphs_test\Form\TestEmbeddedEntityForm;

/**
 * Tests the langcode change mechanics of paragraphs.
 *
 * @group paragraphs
 */
class ParagraphsLangcodeChangeTest extends EntityKernelTestBase {

  /**
   * Modules to enable.
   *
   * @var array
   */
  protected static $modules = [
    'user',
    'system',
    'field',
    'text',
    'filter',
    'entity_test',
    'paragraphs',
    'paragraphs_test',
    'entity_reference_revisions',
    'node',
    'language',
    'file',
  ];

  /**
   * The machine name of the node type.
   *
   * @var string
   */
  protected $nodeType = 'page';

  /**
   * The machine name of the node's paragraphs field.
   *
   * @var string
   */
  protected $nodeParagraphsFieldName = 'field_paragraphs';

  /**
   * The machine name of the paragraph type.
   *
   * @var string
   */
  protected $paragraphType = 'paragraph_type';

  /**
   * The current node.
   *
   * @var \Drupal\node\NodeInterface
   */
  protected $node;

  /**
   * The current paragraph.
   *
   * @var \Drupal\paragraphs\ParagraphInterface
   */
  protected $paragraph;

  /**
   * The array of the current form.
   *
   * @var array
   */
  protected $form;

  /**
   * The current form state.
   *
   * @var \Drupal\Core\Form\FormStateInterface
   */
  protected $formState;

  /**
   * The current form object.
   *
   * @var \Drupal\Core\Entity\ContentEntityFormInterface
   */
  protected $formObject;

  /**
   * The current entity form display.
   *
   * @var \Drupal\Core\Entity\Entity\EntityFormDisplay
   */
  protected $formDisplay;

  /**
   * The current form builder.
   *
   * @var \Drupal\Core\Form\FormBuilderInterface
   */
  protected $formBuilder;

  /**
   * {@inheritdoc}
   */
  protected function setUp(): void {
    parent::setUp();

    $this->installEntitySchema('file');
    $this->installEntitySchema('node');
    $this->installEntitySchema('paragraph');

    $this->installSchema('node', ['node_access']);

    $this->installConfig(static::$modules);

    $this->formBuilder = $this->container->get('form_builder');

    // Activate Spanish language, so there are two languages activated.
    $this->entityTypeManager->getStorage('configurable_language')->create([
      'id' => 'es',
    ])->save();

    // Create a paragraph type.
    $this->entityTypeManager->getStorage('paragraphs_type')->create([
      'label' => 'Paragraph type',
      'id' => $this->paragraphType,
      'status' => TRUE,
    ])->save();

    // Create a node type.
    $this->entityTypeManager->getStorage('node_type')->create([
      'name' => 'Example page',
      'type' => $this->nodeType,
      'create_body' => FALSE,
    ])->save();

    // Enable translations on the node type and paragraph type.
    ContentLanguageSettings::loadByEntityTypeBundle('node', $this->nodeType)
      ->setLanguageAlterable(TRUE)
      ->setDefaultLangcode('en')
      ->save();
    ContentLanguageSettings::loadByEntityTypeBundle('paragraph', $this->paragraphType)
      ->setLanguageAlterable(TRUE)
      ->setDefaultLangcode('en')
      ->save();

    // Create a field with paragraphs for the node type.
    FieldStorageConfig::create([
      'entity_type' => 'node',
      'type' => 'entity_reference_revisions',
      'field_name' => $this->nodeParagraphsFieldName,
      'settings' => [
        'target_type' => 'paragraph',
      ],
      'cardinality' => -1,
      'translatable' => TRUE,
    ])->save();
    FieldConfig::create([
      'entity_type' => 'node',
      'bundle' => $this->nodeType,
      'field_name' => $this->nodeParagraphsFieldName,
      'label' => $this->randomString(),
      'settings' => [
        'handler' => 'default:paragraph',
        'handler_settings' => [
          'negate' => 1,
          'target_bundles' => NULL,
          'target_bundles_drag_drop' => [
            $this->paragraphType => [
              'weight' => 0,
              'enabled' => FALSE,
            ],
          ],
        ],
      ],
    ])->save();

    // Create the form display of the node type,
    // with the language switcher enabled.
    // The default autocomplete widget does not work properly
    // within a kernel test. Thus, use a simple select list widget instead.
    EntityFormDisplay::create([
      'targetEntityType' => 'node',
      'bundle' => $this->nodeType,
      'mode' => 'default',
      'status' => TRUE,
    ])->setComponent('langcode', [
      'type' => 'language_select',
      'region' => 'content',
      'weight' => 10,
    ])->setComponent('uid', [
      'type' => 'options_select',
      'region' => 'content',
      'weight' => 100,
    ])->save();

    $this->formDisplay = EntityFormDisplay::load('node.' . $this->nodeType . '.default');

    $this->createUser(['uid' => 1, 'name' => 'user1'])->save();

    $this->paragraph = $this->entityTypeManager->getStorage('paragraph')->create([
      'type' => $this->paragraphType,
    ]);

    $this->node = $this->entityTypeManager->getStorage('node')->create([
      'type' => $this->nodeType,
      'title' => $this->randomString(),
      'status' => TRUE,
      'uid' => 1,
      'langcode' => 'es',
      $this->nodeParagraphsFieldName => [$this->paragraph],
    ]);
  }

  /**
   * Tests the langcode change within a node form using the legacy widget.
   */
  public function testChangeWithLegacyWidget() {
    $this->doTestLangcodeChange(
      [
        'type' => 'entity_reference_paragraphs',
        'weight' => 5,
      ],
      FALSE
    );
  }

  /**
   * Tests the langcode change within a node form using the stable widget.
   */
  public function testChangeWithStableWidget() {
    $this->doTestLangcodeChange(
      [
        'type' => 'paragraphs',
        'weight' => 15,
      ],
      FALSE
    );
  }

  /**
   * Tests langcode change within an embedded node form and the legacy widget.
   */
  public function testChangeWithEmbeddedLegacyWidget() {
    $this->doTestLangcodeChange(
      [
        'type' => 'entity_reference_paragraphs',
        'weight' => 5,
      ],
      TRUE
    );
  }

  /**
   * Tests langcode change within an embedded node form and the stable widget.
   */
  public function testChangeWithEmbeddedStableWidget() {
    $this->doTestLangcodeChange(
      [
        'type' => 'paragraphs',
        'weight' => 15,
      ],
      TRUE
    );
  }

  /**
   * Performs the test run with the given options.
   *
   * @param array $widget_options
   *   The paragraph widget options.
   * @param bool $embedded
   *   (Optional) Whether the embedded form should be used or not.
   */
  protected function doTestLangcodeChange(array $widget_options, $embedded = FALSE) {
    $this->formDisplay
      ->setComponent($this->nodeParagraphsFieldName, $widget_options)
      ->save();
    $this->doTestChangeWithinNodeForm($embedded);
  }

  /**
   * Performs the test run regards the node form.
   *
   * @param bool $embedded
   *   (Optional) Whether the embedded form should be used or not.
   */
  protected function doTestChangeWithinNodeForm($embedded = FALSE) {
    $this->assertEquals('es', $this->node->language()->getId(), "The node was created with langcode es.");
    $this->assertEquals('en', $this->paragraph->language()->getId(), "The paragraph was created with its default langcode en.");

    // Use this form to add a node.
    $this->buildNodeForm($embedded);

    $this->submitNodeForm();

    $langcode = $this->node->language()->getId();
    $this->assertEquals('es', $langcode, "The node's langcode remains unchanged to value es (after submission).");
    $this->assertEquals($langcode, $this->paragraph->language()->getId(), "The paragraph's langcode was inherited from its parent (after submission).");

    // Switch to the form again.
    $this->buildNodeForm($embedded);

    // Change the node's language from es to en.
    if ($embedded) {
      $this->formState->setValue(['embedded_entity_form', 'langcode'], [['value' => 'en']]);
    }
    else {
      $this->formState->setValue('langcode', [['value' => 'en']]);
    }
    $this->submitNodeForm();

    $langcode = $this->node->language()->getId();
    $this->assertEquals('en', $langcode, "The node's langcode was updated to value en (after submission).");
    $this->assertEquals($langcode, $this->paragraph->language()->getId(), "The paragraph's updated langcode was inherited from its parent (after submission).");

    // Rebuild the form once more and make sure
    // that the langcode change does not get lost.
    $this->buildNodeForm($embedded);

    // Change the node's language from en to es.
    if ($embedded) {
      $this->formState->setValue(['embedded_entity_form', 'langcode'], [['value' => 'es']]);
    }
    else {
      $this->formState->setValue('langcode', [['value' => 'es']]);
    }

    $this->submitNodeForm();

    $langcode = $this->node->language()->getId();
    $this->assertEquals('es', $langcode, "The node's langcode was set to es (after rebuild and submission).");
    $this->assertEquals($langcode, $this->paragraph->language()->getId(), "The paragraph's langcode was inherited from its parent (after rebuild and submission).");
  }

  /**
   * Builds the node form.
   *
   * @param bool $embedded
   *   (Optional) Whether the embedded form should be used or not.
   */
  protected function buildNodeForm($embedded = FALSE) {
    if ($embedded) {
      $this->formObject = new TestEmbeddedEntityForm($this->node);
    }
    else {
      $this->formObject = $this->entityTypeManager->getFormObject('node', 'default');
      $this->formObject->setEntity($this->node);
    }
    $this->formState = (new FormState())
      ->disableRedirect()
      ->setFormObject($this->formObject);
    $this->form = $this->formBuilder->buildForm($this->formObject, $this->formState);
    $this->reassignEntities();
  }

  /**
   * Submits the node form, emulating the save operation as triggering element.
   */
  protected function submitNodeForm() {
    // Submit the form with the default save operation.
    $this->formState->setValue('op', $this->formState->getValue('submit'));
    $this->formBuilder->submitForm($this->formObject, $this->formState);
    $this->reassignEntities();
  }

  /**
   * Helper method to reassign the current entity objects.
   */
  protected function reassignEntities() {
    $this->node = $this->formObject->getEntity();
    $paragraphs = $this->node->get($this->nodeParagraphsFieldName)->referencedEntities();
    $this->paragraph = reset($paragraphs);
  }

}