diff --git a/core/modules/media_library/src/Form/AddFormBase.php b/core/modules/media_library/src/Form/AddFormBase.php index 9a1b2a1544..0074461d60 100644 --- a/core/modules/media_library/src/Form/AddFormBase.php +++ b/core/modules/media_library/src/Form/AddFormBase.php @@ -5,6 +5,7 @@ use Drupal\Core\Ajax\AjaxResponse; use Drupal\Core\Ajax\CloseDialogCommand; use Drupal\Core\Ajax\InvokeCommand; +use Drupal\Core\Ajax\MessageCommand; use Drupal\Core\Ajax\ReplaceCommand; use Drupal\Core\Entity\Entity\EntityFormDisplay; use Drupal\Core\Entity\EntityStorageInterface; @@ -705,11 +706,29 @@ public function updateLibrary(array &$form, FormStateInterface $form_state) { return $media->id(); }, $this->getAddedMediaItems($form_state)); + if ($current_selection = $form_state->getValue('current_selection')) { + $current_selection = $current_selection . ',' . implode(',', $media_ids); + } + else { + $current_selection = implode(',', $media_ids); + } + $response = new AjaxResponse(); $response->addCommand(new UpdateSelectionCommand($media_ids)); $media_id_to_focus = array_pop($media_ids); $response->addCommand(new ReplaceCommand('#media-library-add-form-wrapper', $this->buildMediaLibraryUi($form_state))); $response->addCommand(new InvokeCommand("#media-library-content [value=$media_id_to_focus]", 'focus')); + + $available_slots = $this->getMediaLibraryState($form_state)->getAvailableSlots(); + $selected_count = count(explode(',', $current_selection)); + if ($available_slots > 0 && $selected_count > $available_slots) { + $warning = $this->formatPlural($selected_count - $available_slots, 'There are currently @total items selected, but the maximum number of remaining items for the field is @max. Please remove @count item from the selection.', 'There are currently @total items selected. The maximum number of remaining items for the field is @max. Please remove @count items from the selection.', [ + '@total' => $selected_count, + '@max' => $available_slots, + ]); + $response->addCommand(new MessageCommand($warning, '#media-library-item-count', ['type' => 'warning'])); + } + return $response; } @@ -755,14 +774,30 @@ public function updateWidget(array &$form, FormStateInterface $form_state) { // The added media items get an ID when they are saved in ::submitForm(). // For that reason the added media items are keyed by delta in the form // state and we have to do an array map to get each media ID. - $current_media_ids = array_map(function (MediaInterface $media) { + $media_ids = array_map(function (MediaInterface $media) { return $media->id(); }, $this->getCurrentMediaItems($form_state)); // Allow the opener service to respond to the selection. $state = $this->getMediaLibraryState($form_state); + + + if ($current_selection = $form_state->getValue('current_selection')) { + $current_selection = $current_selection . ',' . implode(',', $media_ids); + } + else { + $current_selection = implode(',', $media_ids); + } + + $available_slots = $this->getMediaLibraryState($form_state)->getAvailableSlots(); + $selected_count = count(explode(',', $current_selection)); + if ($available_slots > 0 && $selected_count > $available_slots) { + // Return to library where we display a warning about the overage. + return $this->updateLibrary($form, $form_state); + } + return $this->openerResolver->get($state) - ->getSelectionResponse($state, $current_media_ids) + ->getSelectionResponse($state, $media_ids) ->addCommand(new CloseDialogCommand()); } diff --git a/core/modules/media_library/src/Form/FileUploadForm.php b/core/modules/media_library/src/Form/FileUploadForm.php index 3299b84e15..208ef00477 100644 --- a/core/modules/media_library/src/Form/FileUploadForm.php +++ b/core/modules/media_library/src/Form/FileUploadForm.php @@ -157,8 +157,10 @@ protected function buildInputElement(array $form, FormStateInterface $form_state // @todo Move validation in https://www.drupal.org/node/2988215 '#process' => array_merge(['::validateUploadElement'], $process, ['::processUploadElement']), '#upload_validators' => $item->getUploadValidators(), - '#multiple' => $slots > 1 || $slots === FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, - '#cardinality' => $slots, + '#multiple' => TRUE, + // Do not limit the number uploaded. There is validation based on the + // number selected in the media library that prevents overages. + '#cardinality' => -1, '#remaining_slots' => $slots, ]; diff --git a/core/modules/media_library/src/Plugin/views/field/MediaLibrarySelectForm.php b/core/modules/media_library/src/Plugin/views/field/MediaLibrarySelectForm.php index 2d0702bd4d..dbf9b0d53f 100644 --- a/core/modules/media_library/src/Plugin/views/field/MediaLibrarySelectForm.php +++ b/core/modules/media_library/src/Plugin/views/field/MediaLibrarySelectForm.php @@ -2,7 +2,9 @@ namespace Drupal\media_library\Plugin\views\field; +use Drupal\Core\Ajax\AjaxResponse; use Drupal\Core\Ajax\CloseDialogCommand; +use Drupal\Core\Ajax\MessageCommand; use Drupal\Core\Form\FormBuilderInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Url; @@ -47,6 +49,15 @@ public function render(ResultRow $values) { public function viewsForm(array &$form, FormStateInterface $form_state) { $form['#attributes']['class'] = ['js-media-library-views-form']; + // Add target for ajax messages. + $form['media_library_messages'] = [ + '#type' => 'container', + '#attributes' => [ + 'id' => 'media-library-item-count', + ], + '#weight' => -10, + ]; + // Add an attribute that identifies the media type displayed in the form. if (isset($this->view->args[0])) { $form['#attributes']['data-drupal-media-type'] = $this->view->args[0]; @@ -127,6 +138,19 @@ public static function updateWidget(array &$form, FormStateInterface $form_state // Allow the opener service to handle the selection. $state = MediaLibraryState::fromRequest($request); + $current_selection = $form_state->getValue('media_library_select_form_selection'); + $available_slots = $state->getAvailableSlots(); + $selected_count = count(explode(',', $current_selection)); + if ($available_slots > 0 && $selected_count > $available_slots) { + $response = new AjaxResponse(); + $error = \Drupal::translation()->formatPlural($selected_count - $available_slots, 'There are currently @total items selected, but the maximum number of remaining items for the field is @max. Please remove @count item from the selection.', 'There are currently @total items selected. The maximum number of remaining items for the field is @max. Please remove @count items from the selection.', [ + '@total' => $selected_count, + '@max' => $available_slots, + ]); + $response->addCommand(new MessageCommand($error, '#media-library-item-count', ['type' => 'error'])); + return $response; + } + return \Drupal::service('media_library.opener_resolver') ->get($state) ->getSelectionResponse($state, $selected_ids) diff --git a/core/modules/media_library/tests/src/FunctionalJavascript/WidgetOverflowTest.php b/core/modules/media_library/tests/src/FunctionalJavascript/WidgetOverflowTest.php new file mode 100644 index 0000000000..587e964922 --- /dev/null +++ b/core/modules/media_library/tests/src/FunctionalJavascript/WidgetOverflowTest.php @@ -0,0 +1,189 @@ +getTestFiles('image') as $image) { + $extension = pathinfo($image->filename, PATHINFO_EXTENSION); + if ($extension === 'png') { + $this->image = $image; + } + } + + if (!isset($this->image)) { + $this->fail('Expected test files not present.'); + } + + // Create a user that can only add media of type four. + $user = $this->drupalCreateUser([ + 'access administration pages', + 'access content', + 'create basic_page content', + 'create type_one media', + 'create type_three media', + 'view media', + ]); + $this->drupalLogin($user); + } + + /** + * Tests overflow validation. + * + * @dataProvider overflowProvider + */ + public function testWidgetOverflow($advanced_ui, $button_text) { + $this->config('media_library.settings')->set('advanced_ui', $advanced_ui)->save(); + $assert_session = $this->assertSession(); + $page = $this->getSession()->getPage(); + $driver = $this->getSession()->getDriver(); + /** @var \Drupal\Core\File\FileSystemInterface $file_system */ + $file_system = $this->container->get('file_system'); + // Visit a node create page and open the media library. + $this->drupalGet('node/add/basic_page'); + $this->openMediaLibraryForField('field_twin_media'); + $assert_session->pageTextContains('Add or select media'); + $assert_session->fieldExists('Add files'); + // Create a list of new files to upload. + $filenames = []; + $remote_paths = []; + foreach (range(1, 5) as $i) { + $path = $file_system->copy($this->image->uri, 'public://'); + $filenames[] = $file_system->basename($path); + $remote_paths[] = $driver->uploadFileAndGetRemoteFilePath($file_system->realpath($path)); + } + $page->findField('Add files')->setValue(implode("\n", $remote_paths)); + // Assert the media item fields are shown and the vertical tabs are no + // longer shown. + $this->assertMediaAdded(); + // Assert all files have been added. + foreach (range(0, 4) as $i) { + $assert_session->fieldValueEquals("media[$i][fields][name][0][value]", $filenames[$i]); + $page->fillField("media[$i][fields][field_media_test_image][0][alt]", $filenames[$i]); + } + // When the user uploads more items than allowed, the media items are + // saved but when the user is returned to the media library there is a + // warning message. + $assert_session->elementExists('css', '.ui-dialog-buttonpane') + ->pressButton($button_text); + $this->waitForText('Please remove 3 items from the selection.'); + $assert_session->elementTextContains('css', '.messages--warning', 'There are currently 5 items selected. The maximum number of remaining items for the field is 2. Please remove 3 items from the selection.'); + // When the user tries insert more items than allowed, the user is returned + // to the media library with an error message. + $assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Insert selected'); + $assert_session->waitForElement('css', '.messages--error'); + $assert_session->elementNotExists('css', '.messages--warning'); + $assert_session->elementTextContains('css', '.messages--error', 'There are currently 5 items selected. The maximum number of remaining items for the field is 2. Please remove 3 items from the selection.'); + // Uncheck the extra items. + $page->uncheckField('media_library_select_form[2]'); + $page->uncheckField('media_library_select_form[3]'); + $page->uncheckField('media_library_select_form[4]'); + $this->pressInsertSelected('Added 2 media items.'); + } + + /** + * Tests overflow validation skips fields with unlimited cardinality. + * + * @dataProvider overflowProvider + */ + public function testUnlimitedCardinality($advanced_ui, $button_text) { + $this->config('media_library.settings')->set('advanced_ui', $advanced_ui)->save(); + $assert_session = $this->assertSession(); + $page = $this->getSession()->getPage(); + $driver = $this->getSession()->getDriver(); + /** @var \Drupal\Core\File\FileSystemInterface $file_system */ + $file_system = $this->container->get('file_system'); + // Visit a node create page and open the media library. + $this->drupalGet('node/add/basic_page'); + $this->openMediaLibraryForField('field_unlimited_media'); + $this->switchToMediaType('Three'); + $assert_session->pageTextContains('Add or select media'); + $assert_session->fieldExists('Add files'); + // Create a list of new files to upload. + $filenames = []; + $remote_paths = []; + foreach (range(1, 5) as $i) { + $path = $file_system->copy($this->image->uri, 'public://'); + $filenames[] = $file_system->basename($path); + $remote_paths[] = $driver->uploadFileAndGetRemoteFilePath($file_system->realpath($path)); + } + $page->findField('Add files')->setValue(implode("\n", $remote_paths)); + // Assert the media item fields are shown and the vertical tabs are no + // longer shown. + $this->assertMediaAdded(); + // Assert all files have been added. + foreach (range(0, 4) as $i) { + $assert_session->fieldValueEquals("media[$i][fields][name][0][value]", $filenames[$i]); + $page->fillField("media[$i][fields][field_media_test_image][0][alt]", $filenames[$i]); + } + // When the user is returned to the media library there should not + // be a warning message. + $buttons = $assert_session->elementExists('css', '.ui-dialog-buttonpane'); + $buttons->pressButton($button_text); + + if ($button_text === 'Save and insert') { + $this->waitForText('Added 5 media items.'); + } + else { + $result = $buttons->waitFor(10, function ($buttons) { + /** @var \Behat\Mink\Element\NodeElement $buttons */ + return $buttons->findButton('Insert selected'); + }); + $this->assertNotEmpty($result); + $assert_session->elementNotExists('css', '.messages--warning'); + // When the user tries insert more items than allowed, + // the user is returned + // to the media library with an error message. + $this->pressInsertSelected('Added 5 media items.'); + } + } + + /** + * Data provider for ::testWidgetOverflow(). + * + * @return array + * Test data. + */ + public function overflowProvider() { + return [ + 'Save button' => [ + 'advancedUi' => FALSE, + 'buttonText' => 'Save', + ], + 'Save and insert button' => [ + 'advancedUi' => TRUE, + 'buttonText' => 'Save and insert', + ], + 'Save and select button' => [ + 'advancedUi' => TRUE, + 'buttonText' => 'Save and select', + ], + ]; + } + +} diff --git a/core/modules/media_library/tests/src/FunctionalJavascript/WidgetUploadTest.php b/core/modules/media_library/tests/src/FunctionalJavascript/WidgetUploadTest.php index 353629fc14..24ce75e21c 100644 --- a/core/modules/media_library/tests/src/FunctionalJavascript/WidgetUploadTest.php +++ b/core/modules/media_library/tests/src/FunctionalJavascript/WidgetUploadTest.php @@ -197,7 +197,8 @@ public function testWidgetUpload() { // Assert we can now only upload one more media item. $this->openMediaLibraryForField('field_twin_media'); $this->switchToMediaType('Four'); - $this->assertFalse($assert_session->fieldExists('Add file')->hasAttribute('multiple')); + // Despite the 'One file only' text, we don't limit the number of uploads. + $this->assertTrue($assert_session->fieldExists('Add file')->hasAttribute('multiple')); $assert_session->pageTextContains('One file only.'); // Assert media type four should only allow jpg files by trying a png file @@ -538,7 +539,8 @@ public function testWidgetUploadAdvancedUi() { // Assert we can now only upload one more media item. $this->openMediaLibraryForField('field_twin_media'); $this->switchToMediaType('Four'); - $this->assertFalse($assert_session->fieldExists('Add file')->hasAttribute('multiple')); + // Despite the 'One file only' text, we don't limit the number of uploads. + $this->assertTrue($assert_session->fieldExists('Add file')->hasAttribute('multiple')); $assert_session->pageTextContains('One file only.'); // Assert media type four should only allow jpg files by trying a png file @@ -610,7 +612,9 @@ public function testWidgetUploadAdvancedUi() { $selection_area = $this->getSelectionArea(); $assert_session->checkboxChecked("Select $existing_media_name", $selection_area); $selection_area->uncheckField("Select $existing_media_name"); - $assert_session->hiddenFieldValueEquals('current_selection', ''); + $page->waitFor(10, function () use ($page) { + return $page->find('hidden_field_selector', ['hidden_field', 'current_selection'])->getValue() === ''; + }); // Close the details element so that clicking the Save and select works. // @todo Fix dialog or test so this is not necessary to prevent random // fails. https://www.drupal.org/project/drupal/issues/3055648