From 6470ab797cbeca91318c5c5e7974ff419d77cea9 Mon Sep 17 00:00:00 2001
From: Alex Pott <alex.a.pott@googlemail.com>
Date: Tue, 1 Feb 2022 12:31:23 +0000
Subject: [PATCH] Issue #3115054 by chr.fritsch, vsujeetkumar, Vidushi Mehta,
 sergiuteaca, janmejaig, ranjith_kumar_k_u, phenaproxima: Media library widget
 forgets ordering when adding or removing items

---
 .../Field/FieldWidget/MediaLibraryWidget.php  | 34 ++++++++
 .../EntityReferenceWidgetTest.php             | 86 +++++++++++++++++++
 2 files changed, 120 insertions(+)

diff --git a/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
index 3f341f54b06c..7e76940a0b35 100644
--- a/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
+++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
@@ -286,6 +286,22 @@ public function form(FieldItemListInterface $items, array &$form, FormStateInter
     return parent::form($items, $form, $form_state, $get_delta);
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function extractFormValues(FieldItemListInterface $items, array $form, FormStateInterface $form_state) {
+    parent::extractFormValues($items, $form, $form_state);
+
+    // Update reference to 'items' stored during add or remove to take into
+    // account changes to values like 'weight' etc.
+    // @see Drupal\media_library\Plugin\Field\FieldWidget\MediaLibraryWidget::addItems
+    // @see Drupal\media_library\Plugin\Field\FieldWidget\MediaLibraryWidget::removeItem
+    $field_name = $this->fieldDefinition->getName();
+    $field_state = static::getWidgetState($form['#parents'], $field_name, $form_state);
+    $field_state['items'] = $items->getValue();
+    static::setWidgetState($form['#parents'], $field_name, $form_state, $field_state);
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -744,7 +760,15 @@ public static function updateWidget(array $form, FormStateInterface $form_state)
    *   The form state.
    */
   public static function removeItem(array $form, FormStateInterface $form_state) {
+    // During the form rebuild, formElement() will create field item widget
+    // elements using re-indexed deltas, so clear out FormState::$input to
+    // avoid a mismatch between old and new deltas. The rebuilt elements will
+    // have #default_value set appropriately for the current state of the field,
+    // so nothing is lost in doing this.
+    // @see Drupal\media_library\Plugin\Field\FieldWidget\MediaLibraryWidget::extractFormValues
     $triggering_element = $form_state->getTriggeringElement();
+    $parents = array_slice($triggering_element['#parents'], 0, -2);
+    NestedArray::setValue($form_state->getUserInput(), $parents, NULL);
 
     // Get the parents required to find the top-level widget element.
     if (count($triggering_element['#array_parents']) < 4) {
@@ -844,7 +868,17 @@ public static function validateItems(array $form, FormStateInterface $form_state
    *   The form state.
    */
   public static function addItems(array $form, FormStateInterface $form_state) {
+    // During the form rebuild, formElement() will create field item widget
+    // elements using re-indexed deltas, so clear out FormState::$input to
+    // avoid a mismatch between old and new deltas. The rebuilt elements will
+    // have #default_value set appropriately for the current state of the field,
+    // so nothing is lost in doing this.
+    // @see Drupal\media_library\Plugin\Field\FieldWidget\MediaLibraryWidget::extractFormValues
     $button = $form_state->getTriggeringElement();
+    $parents = array_slice($button['#parents'], 0, -1);
+    $parents[] = 'selection';
+    NestedArray::setValue($form_state->getUserInput(), $parents, NULL);
+
     $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -1));
 
     $field_state = static::getFieldState($element, $form_state);
diff --git a/core/modules/media_library/tests/src/FunctionalJavascript/EntityReferenceWidgetTest.php b/core/modules/media_library/tests/src/FunctionalJavascript/EntityReferenceWidgetTest.php
index 5ec06908ffec..31c00338290c 100644
--- a/core/modules/media_library/tests/src/FunctionalJavascript/EntityReferenceWidgetTest.php
+++ b/core/modules/media_library/tests/src/FunctionalJavascript/EntityReferenceWidgetTest.php
@@ -3,6 +3,7 @@
 namespace Drupal\Tests\media_library\FunctionalJavascript;
 
 use Drupal\field\Entity\FieldConfig;
+use Drupal\FunctionalJavascriptTests\SortableTestTrait;
 
 /**
  * Tests the Media library entity reference widget.
@@ -11,6 +12,8 @@
  */
 class EntityReferenceWidgetTest extends MediaLibraryTestBase {
 
+  use SortableTestTrait;
+
   /**
    * {@inheritdoc}
    */
@@ -256,6 +259,7 @@ public function testWidget() {
     $this->openMediaLibraryForField('field_twin_media');
     $page->checkField('Select Dog');
     $this->pressInsertSelected('Added one media item.');
+    $this->waitForElementsCount('css', '.field--name-field-twin-media [data-media-library-item-delta]', 2);
     // Assert that we can toggle the visibility of the weight inputs when the
     // field contains more than one item.
     $wrapper = $assert_session->elementExists('css', '.field--name-field-twin-media');
@@ -485,4 +489,86 @@ public function testRequiredMediaField() {
     $this->assertSession()->pageTextContains('Basic page My page has been created.');
   }
 
+  /**
+   * Tests that changed order is maintained after removing a selection.
+   */
+  public function testRemoveAfterReordering(): void {
+    $assert_session = $this->assertSession();
+    $page = $this->getSession()->getPage();
+
+    $this->drupalGet('node/add/basic_page');
+    $page->fillField('Title', 'My page');
+
+    $this->openMediaLibraryForField('field_unlimited_media');
+    $page->checkField('Select Dog');
+    $page->checkField('Select Cat');
+    $page->checkField('Select Bear');
+    // Order: Dog - Cat - Bear.
+    $this->pressInsertSelected('Added 3 media items.');
+
+    // Move first item (Dog) to the end.
+    // Order: Cat - Bear - Dog.
+    $this->sortableAfter('[data-media-library-item-delta="0"]', '[data-media-library-item-delta="2"]', '.js-media-library-selection');
+
+    $wrapper = $assert_session->elementExists('css', '.field--name-field-unlimited-media');
+    // Remove second item (Bear).
+    // Order: Cat - Dog.
+    $wrapper->find('css', "[aria-label='Remove Bear']")->press();
+    $this->waitForText('Bear has been removed.');
+    $page->pressButton('Save');
+
+    $assert_session->elementTextContains('css', '.field--name-field-unlimited-media > .field__items > .field__item:last-child', 'Dog');
+  }
+
+  /**
+   * Tests that order is correct after re-order and adding another item.
+   */
+  public function testAddAfterReordering(): void {
+    $assert_session = $this->assertSession();
+    $page = $this->getSession()->getPage();
+
+    $this->drupalGet('node/add/basic_page');
+    $page->fillField('Title', 'My page');
+
+    $this->openMediaLibraryForField('field_unlimited_media');
+    $page->checkField('Select Dog');
+    $page->checkField('Select Cat');
+    // Order: Dog - Cat.
+    $this->pressInsertSelected('Added 2 media items.');
+
+    // Change positions.
+    // Order: Cat - Dog.
+    $this->sortableAfter('[data-media-library-item-delta="0"]', '[data-media-library-item-delta="1"]', '.js-media-library-selection');
+
+    $this->openMediaLibraryForField('field_unlimited_media');
+    $this->selectMediaItem(2);
+    // Order: Cat - Dog - Bear.
+    $this->pressInsertSelected('Added one media item.');
+
+    $page->pressButton('Save');
+
+    $assert_session->elementTextContains('css', '.field--name-field-unlimited-media > .field__items > .field__item:first-child', 'Cat');
+    $assert_session->elementTextContains('css', '.field--name-field-unlimited-media > .field__items > .field__item:last-child', 'Bear');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function sortableUpdate($item, $from, $to = NULL) {
+    // See core/modules/media_library/js/media_library.widget.es6.js.
+    $script = <<<JS
+(function ($) {
+    var selection = document.querySelectorAll('.js-media-library-selection');
+    selection.forEach(function (widget) {
+        $(widget).children().each(function (index, child) {
+            $(child).find('.js-media-library-item-weight').val(index);
+        });
+    });
+})(jQuery)
+
+JS;
+
+    $this->getSession()->executeScript($script);
+  }
+
 }
-- 
GitLab