From 792a14f5bb5c169b60cb0e473400a33ad8a304ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=CC=81bor=20Hojtsy?= <gabor@hojtsy.hu> Date: Mon, 18 Feb 2019 11:37:04 +0100 Subject: [PATCH] Issue #3023802 by seanB, phenaproxima, lauriii, Pancho, larowlan, shaal, dww: Show media add form directly on media type tab in media library --- .../media/src/Annotation/MediaSource.php | 10 + .../media_library/css/media_library.theme.css | 48 +- .../media_library/js/media_library.ui.es6.js | 20 + .../media_library/js/media_library.ui.js | 6 + .../media_library/media_library.module | 43 +- .../media_library/media_library.routing.yml | 6 - .../media_library/media_library.services.yml | 2 +- .../src/Ajax/UpdateSelectionCommand.php | 54 ++ .../media_library/src/Form/AddFormBase.php | 447 ++++++++++++ .../media_library/src/Form/FileUploadForm.php | 248 +++++++ .../src/Form/MediaLibraryUploadForm.php | 639 ------------------ .../media_library/src/MediaLibraryState.php | 5 +- .../src/MediaLibraryUiBuilder.php | 80 ++- .../Field/FieldWidget/MediaLibraryWidget.php | 3 + .../views/field/MediaLibrarySelectForm.php | 3 + .../FunctionalJavascript/MediaLibraryTest.php | 187 +++-- .../src/Kernel/MediaLibraryAddFormTest.php | 111 +++ 17 files changed, 1165 insertions(+), 747 deletions(-) create mode 100644 core/modules/media_library/src/Ajax/UpdateSelectionCommand.php create mode 100644 core/modules/media_library/src/Form/AddFormBase.php create mode 100644 core/modules/media_library/src/Form/FileUploadForm.php delete mode 100644 core/modules/media_library/src/Form/MediaLibraryUploadForm.php create mode 100644 core/modules/media_library/tests/src/Kernel/MediaLibraryAddFormTest.php diff --git a/core/modules/media/src/Annotation/MediaSource.php b/core/modules/media/src/Annotation/MediaSource.php index 2f7bdad23d40..1286601b2967 100644 --- a/core/modules/media/src/Annotation/MediaSource.php +++ b/core/modules/media/src/Annotation/MediaSource.php @@ -57,6 +57,16 @@ class MediaSource extends Plugin { */ public $allowed_field_types = []; + /** + * The classes used to define media source-specific forms. + * + * An array of form class names, keyed by ID. The ID represents the operation + * the form is used for. + * + * @var string[] + */ + public $forms = []; + /** * A filename for the default thumbnail. * diff --git a/core/modules/media_library/css/media_library.theme.css b/core/modules/media_library/css/media_library.theme.css index 61be1a750795..e5ca9ab6d5d7 100644 --- a/core/modules/media_library/css/media_library.theme.css +++ b/core/modules/media_library/css/media_library.theme.css @@ -83,6 +83,19 @@ border-left: 0; } +.media-library-add-form--without-input { + margin-bottom: 1em; + border-bottom: 1px solid #c0c0c0; +} + +.media-library-add-form--without-input .form-item { + margin: 0 0 1em; +} + +.media-library-add-form .file-upload-help { + margin: 8px 0 0; +} + .media-library-views-form__header .form-item { margin-right: 8px; } @@ -276,31 +289,34 @@ border-color: #40b6ff; } -/* Style the wrappers around new media and files */ -.media-library-upload__media, -.media-library-upload__file { +/* Style the wrappers around new media and files. */ +.media-library-add-form__media { display: flex; padding: 20px 0 20px 0; + border-bottom: 1px solid #c0c0c0; } -.media-library-upload__file { - align-items: center; +/* Do not show the top padding for the first item. */ +.media-library-add-form__media:first-child { + padding-top: 0; } -.media-library-upload__file-label { - margin-right: 10px; +/* Do not show the bottom border and padding for the last item. */ +.media-library-add-form__media:last-child { + border-bottom: 0; + padding-bottom: 0; } /* @todo Remove in https://www.drupal.org/project/drupal/issues/2987921 */ -.media-library-upload__source-field .file, -.media-library-upload__source-field .button, -.media-library-upload__source-field .image-preview, -.media-library-upload__source-field .form-type-managed-file > label, -.media-library-upload__source-field .file-size { +.media-library-add-form__source-field .file, +.media-library-add-form__source-field .button, +.media-library-add-form__source-field .image-preview, +.media-library-add-form__source-field .form-type-managed-file > label, +.media-library-add-form__source-field .file-size { display: none; } -.media-library-upload__media-preview { +.media-library-add-form__preview { display: flex; justify-content: center; align-items: center; @@ -308,15 +324,11 @@ margin-right: 20px; background: #ebebeb; } -[dir="rtl"] .media-library-upload__media-preview { +[dir="rtl"] .media-library-add-form__preview { margin-right: 0; margin-left: 20px; } -.media-library-upload__media-preview img { - display: block; -} - /* @todo Remove or re-work in https://www.drupal.org/node/2985168 */ .media-library-widget .media-library-item__name a, .media-library-view.view-display-id-widget .media-library-item__name a { diff --git a/core/modules/media_library/js/media_library.ui.es6.js b/core/modules/media_library/js/media_library.ui.es6.js index d0aff3baf95c..f0676d10136d 100644 --- a/core/modules/media_library/js/media_library.ui.es6.js +++ b/core/modules/media_library/js/media_library.ui.es6.js @@ -15,6 +15,26 @@ currentSelection: [], }; + /** + * Command to update the current media library selection. + * + * @param {Drupal.Ajax} [ajax] + * The Drupal Ajax object. + * @param {object} response + * Object holding the server response. + * @param {number} [status] + * The HTTP status code. + */ + Drupal.AjaxCommands.prototype.updateMediaLibrarySelection = function( + ajax, + response, + status, + ) { + Object.values(response.mediaIds).forEach(value => { + Drupal.MediaLibrary.currentSelection.push(value); + }); + }; + /** * Warn users when clicking outgoing links from the library or widget. * diff --git a/core/modules/media_library/js/media_library.ui.js b/core/modules/media_library/js/media_library.ui.js index 163f989bdca8..4d3e91693459 100644 --- a/core/modules/media_library/js/media_library.ui.js +++ b/core/modules/media_library/js/media_library.ui.js @@ -10,6 +10,12 @@ currentSelection: [] }; + Drupal.AjaxCommands.prototype.updateMediaLibrarySelection = function (ajax, response, status) { + Object.values(response.mediaIds).forEach(function (value) { + Drupal.MediaLibrary.currentSelection.push(value); + }); + }; + Drupal.behaviors.MediaLibraryWidgetWarn = { attach: function attach(context) { $('.js-media-library-item a[href]', context).once('media-library-warn-link').on('click', function (e) { diff --git a/core/modules/media_library/media_library.module b/core/modules/media_library/media_library.module index b5d5d1d3c761..6533258899b7 100644 --- a/core/modules/media_library/media_library.module +++ b/core/modules/media_library/media_library.module @@ -5,7 +5,6 @@ * Contains hook implementations for the media_library module. */ -use Drupal\Component\Serialization\Json; use Drupal\Component\Utility\UrlHelper; use Drupal\Core\Access\AccessResult; use Drupal\Core\Entity\EntityInterface; @@ -17,11 +16,11 @@ use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\Template\Attribute; -use Drupal\Core\Url; use Drupal\image\Entity\ImageStyle; use Drupal\image\Plugin\Field\FieldType\ImageItem; use Drupal\media\MediaTypeForm; use Drupal\media\MediaTypeInterface; +use Drupal\media_library\Form\FileUploadForm; use Drupal\media_library\MediaLibraryState; use Drupal\views\Form\ViewsForm; use Drupal\views\Plugin\views\cache\CachePluginBase; @@ -41,6 +40,16 @@ function media_library_help($route_name, RouteMatchInterface $route_match) { } } +/** + * Implements hook_media_source_info_alter(). + */ +function media_library_media_source_info_alter(array &$sources) { + $sources['audio_file']['forms']['media_library_add'] = FileUploadForm::class; + $sources['file']['forms']['media_library_add'] = FileUploadForm::class; + $sources['image']['forms']['media_library_add'] = FileUploadForm::class; + $sources['video_file']['forms']['media_library_add'] = FileUploadForm::class; +} + /** * Implements hook_theme(). */ @@ -52,36 +61,6 @@ function media_library_theme() { ]; } -/** - * Implements hook_preprocess_view(). - * - * Adds a link to add media above the view. - */ -function media_library_preprocess_views_view(&$variables) { - $view = $variables['view']; - if ($view->id() === 'media_library' && $view->current_display === 'widget') { - $url = Url::fromRoute('media_library.upload'); - if ($url->access()) { - $url->setOption('query', \Drupal::request()->query->all()); - $variables['header']['add_media'] = [ - '#type' => 'link', - '#title' => t('Add media'), - '#url' => $url, - '#attributes' => [ - 'class' => ['button', 'button-action', 'button--primary', 'use-ajax'], - 'data-dialog-type' => 'modal', - 'data-dialog-options' => Json::encode([ - 'dialogClass' => 'media-library-widget-modal', - 'height' => '75%', - 'width' => '75%', - 'title' => t('Add media'), - ]), - ], - ]; - } - } -} - /** * Implements hook_views_post_render(). */ diff --git a/core/modules/media_library/media_library.routing.yml b/core/modules/media_library/media_library.routing.yml index 8f0fb5f87e12..efc9836e4702 100644 --- a/core/modules/media_library/media_library.routing.yml +++ b/core/modules/media_library/media_library.routing.yml @@ -1,9 +1,3 @@ -media_library.upload: - path: '/admin/content/media-widget-upload' - defaults: - _form: '\Drupal\media_library\Form\MediaLibraryUploadForm' - requirements: - _custom_access: '\Drupal\media_library\Form\MediaLibraryUploadForm::access' media_library.ui: path: '/media-library' defaults: diff --git a/core/modules/media_library/media_library.services.yml b/core/modules/media_library/media_library.services.yml index 9550f5190b98..acd28df2493e 100644 --- a/core/modules/media_library/media_library.services.yml +++ b/core/modules/media_library/media_library.services.yml @@ -1,4 +1,4 @@ services: media_library.ui_builder: class: Drupal\media_library\MediaLibraryUiBuilder - arguments: ['@entity_type.manager', '@request_stack', '@views.executable'] + arguments: ['@entity_type.manager', '@request_stack', '@views.executable', '@form_builder'] diff --git a/core/modules/media_library/src/Ajax/UpdateSelectionCommand.php b/core/modules/media_library/src/Ajax/UpdateSelectionCommand.php new file mode 100644 index 000000000000..c9f329ce6bcc --- /dev/null +++ b/core/modules/media_library/src/Ajax/UpdateSelectionCommand.php @@ -0,0 +1,54 @@ +<?php + +namespace Drupal\media_library\Ajax; + +use Drupal\Core\Ajax\CommandInterface; + +/** + * AJAX command for adding media items to the media library selection. + * + * This command instructs the client to add the given media item IDs to the + * current selection of the media library stored in + * Drupal.MediaLibrary.currentSelection. + * + * This command is implemented by + * Drupal.AjaxCommands.prototype.updateMediaLibrarySelection() defined in + * media_library.ui.js. + * + * @ingroup ajax + * + * @internal + * Media Library is an experimental module and its internal code may be + * subject to change in minor releases. External code should not instantiate + * or extend this class. + */ +class UpdateSelectionCommand implements CommandInterface { + + /** + * An array of media IDs to add to the current selection. + * + * @var int[] + */ + protected $mediaIds; + + /** + * Constructs an UpdateSelectionCommand object. + * + * @param int[] $media_ids + * An array of media IDs to add to the current selection. + */ + public function __construct(array $media_ids) { + $this->mediaIds = $media_ids; + } + + /** + * {@inheritdoc} + */ + public function render() { + return [ + 'command' => 'updateMediaLibrarySelection', + 'mediaIds' => $this->mediaIds, + ]; + } + +} diff --git a/core/modules/media_library/src/Form/AddFormBase.php b/core/modules/media_library/src/Form/AddFormBase.php new file mode 100644 index 000000000000..da783d36ab0d --- /dev/null +++ b/core/modules/media_library/src/Form/AddFormBase.php @@ -0,0 +1,447 @@ +<?php + +namespace Drupal\media_library\Form; + +use Drupal\Core\Ajax\AjaxResponse; +use Drupal\Core\Ajax\ReplaceCommand; +use Drupal\Core\Entity\Entity\EntityFormDisplay; +use Drupal\Core\Entity\EntityStorageInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Form\FormBase; +use Drupal\Core\Form\FormStateInterface; +use Drupal\media\MediaInterface; +use Drupal\media\MediaTypeInterface; +use Drupal\media_library\Ajax\UpdateSelectionCommand; +use Drupal\media_library\MediaLibraryState; +use Drupal\media_library\MediaLibraryUiBuilder; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Provides a base class for creating media items from within the media library. + * + * @internal + * Media Library is an experimental module and its internal code may be + * subject to change in minor releases. External code should not instantiate + * or extend this class. + */ +abstract class AddFormBase extends FormBase { + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * The media library UI builder. + * + * @var \Drupal\media_library\MediaLibraryUiBuilder + */ + protected $libraryUiBuilder; + + /** + * The type of media items being created by this form. + * + * @var \Drupal\media\MediaTypeInterface + */ + protected $mediaType; + + /** + * Constructs a AddFormBase object. + * + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + * @param \Drupal\media_library\MediaLibraryUiBuilder $library_ui_builder + * The media library UI builder. + */ + public function __construct(EntityTypeManagerInterface $entity_type_manager, MediaLibraryUiBuilder $library_ui_builder) { + $this->entityTypeManager = $entity_type_manager; + $this->libraryUiBuilder = $library_ui_builder; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_type.manager'), + $container->get('media_library.ui_builder') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'media_library_add_form'; + } + + /** + * Get the media type from the form state. + * + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current form state. + * + * @return \Drupal\media\MediaTypeInterface + * The media type. + */ + protected function getMediaType(FormStateInterface $form_state) { + if ($this->mediaType) { + return $this->mediaType; + } + + $state = $form_state->get('media_library_state'); + + if (!$state) { + throw new \InvalidArgumentException('The media library state is not present in the form state.'); + } + + $selected_type_id = $form_state->get('media_library_state')->getSelectedTypeId(); + $this->mediaType = $this->entityTypeManager->getStorage('media_type')->load($selected_type_id); + + if (!$this->mediaType) { + throw new \InvalidArgumentException("The '$selected_type_id' media type does not exist."); + } + + return $this->mediaType; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $form['#prefix'] = '<div id="media-library-add-form-wrapper">'; + $form['#suffix'] = '</div>'; + $form['#attached']['library'][] = 'media_library/style'; + + // The form is posted via AJAX. When there are messages set during the + // validation or submission of the form, the messages need to be shown to + // the user. + $form['status_messages'] = [ + '#type' => 'status_messages', + ]; + + $form['#attributes']['class'][] = 'media-library-add-form'; + $added_media = $form_state->get('media'); + if (empty($added_media)) { + $form['#attributes']['class'][] = 'media-library-add-form--without-input'; + $form = $this->buildInputElement($form, $form_state); + } + else { + $form['#attributes']['class'][] = 'media-library-add-form--with-input'; + + $form['media'] = [ + '#type' => 'container', + ]; + + foreach ($added_media as $delta => $media) { + $form['media'][$delta] = $this->buildEntityFormElement($media, $form, $form_state, $delta); + } + + $form['actions'] = $this->buildActions($form, $form_state); + } + return $form; + } + + /** + * Builds the element for submitting source field value(s). + * + * The input element needs to have a submit handler to create media items from + * the user input and store them in the form state using + * ::processInputValues(). + * + * @param array $form + * The complete form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current form state. + * + * @return array + * The complete form, with the element added. + * + * @see ::processInputValues() + */ + abstract protected function buildInputElement(array $form, FormStateInterface $form_state); + + /** + * Builds the sub-form for setting required fields on a new media item. + * + * @param \Drupal\media\MediaInterface $media + * A new, unsaved media item. + * @param array $form + * The complete form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current form state. + * @param int $delta + * The delta of the media item. + * + * @return array + * The element containing the required fields sub-form. + */ + protected function buildEntityFormElement(MediaInterface $media, array $form, FormStateInterface $form_state, $delta) { + $element = [ + '#type' => 'container', + '#attributes' => [ + 'class' => [ + 'media-library-add-form__media', + ], + ], + 'preview' => [ + '#type' => 'container', + '#attributes' => [ + 'class' => [ + 'media-library-add-form__preview', + ], + ], + ], + 'fields' => [ + '#type' => 'container', + '#attributes' => [ + 'class' => [ + 'media-library-add-form__fields', + ], + ], + // The '#parents' are set here because the entity form display needs it + // to build the entity form fields. + '#parents' => ['media', $delta, 'fields'], + ], + ]; + // @todo Make the image style configurable in + // https://www.drupal.org/node/2988223 + $source = $media->getSource(); + $plugin_definition = $source->getPluginDefinition(); + if ($thumbnail_uri = $source->getMetadata($media, $plugin_definition['thumbnail_uri_metadata_attribute'])) { + $element['preview']['thumbnail'] = [ + '#theme' => 'image_style', + '#style_name' => 'media_library', + '#uri' => $thumbnail_uri, + ]; + } + + $form_display = EntityFormDisplay::collectRenderDisplay($media, 'media_library'); + // When the name is not added to the form as an editable field, output + // the name as a fixed element to confirm the right file was uploaded. + if (!$form_display->getComponent('name')) { + $element['fields']['name'] = [ + '#type' => 'item', + '#title' => $this->t('Name'), + '#markup' => $media->getName(), + ]; + } + $form_display->buildForm($media, $element['fields'], $form_state); + + // We hide the preview of the uploaded file in the image widget with CSS. + // @todo Improve hiding file widget elements in + // https://www.drupal.org/project/drupal/issues/2987921 + $source_field_name = $this->getSourceFieldName($media->bundle->entity); + if (isset($element['fields'][$source_field_name])) { + $element['fields'][$source_field_name]['#attributes']['class'][] = 'media-library-add-form__source-field'; + } + // The revision log field is currently not configurable from the form + // display, so hide it by changing the access. + // @todo Make the revision_log_message field configurable in + // https://www.drupal.org/project/drupal/issues/2696555 + if (isset($element['fields']['revision_log_message'])) { + $element['fields']['revision_log_message']['#access'] = FALSE; + } + return $element; + } + + /** + * Returns an array of supported actions for the form. + * + * @param array $form + * The complete form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current form state. + * + * @return array + * An actions element containing the actions of the form. + */ + protected function buildActions(array $form, FormStateInterface $form_state) { + return [ + '#type' => 'actions', + 'submit' => [ + '#type' => 'submit', + '#value' => $this->t('Save'), + '#ajax' => [ + 'callback' => '::updateWidget', + 'wrapper' => 'media-library-add-form-wrapper', + ], + ], + ]; + } + + /** + * Creates media items from source field input values. + * + * @param mixed[] $source_field_values + * The values for source fields of the media items. + * @param array $form + * The complete form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current form state. + */ + protected function processInputValues(array $source_field_values, array $form, FormStateInterface $form_state) { + $media_type = $this->getMediaType($form_state); + $media_storage = $this->entityTypeManager->getStorage('media'); + $source_field_name = $this->getSourceFieldName($media_type); + $media = array_map(function ($source_field_value) use ($media_type, $media_storage, $source_field_name) { + return $this->createMediaFromValue($media_type, $media_storage, $source_field_name, $source_field_value); + }, $source_field_values); + $form_state->set('media', $media)->setRebuild(); + } + + /** + * Creates a new, unsaved media item from a source field value. + * + * @param \Drupal\media\MediaTypeInterface $media_type + * The media type of the media item. + * @param \Drupal\Core\Entity\EntityStorageInterface $media_storage + * The media storage. + * @param string $source_field_name + * The name of the media type's source field. + * @param mixed $source_field_value + * The value for the source field of the media item. + * + * @return \Drupal\media\MediaInterface + * An unsaved media entity. + */ + protected function createMediaFromValue(MediaTypeInterface $media_type, EntityStorageInterface $media_storage, $source_field_name, $source_field_value) { + return $media_storage->create([ + 'bundle' => $media_type->id(), + $source_field_name => $source_field_value, + ]); + } + + /** + * Prepares a created media item to be permanently saved. + * + * @param \Drupal\media\MediaInterface $media + * The unsaved media item. + */ + protected function prepareMediaEntityForSave(MediaInterface $media) { + // Intentionally empty by default. + } + + /** + * AJAX callback to update the entire form based on source field input. + * + * @param array $form + * The complete form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current form state. + * + * @return \Drupal\Core\Ajax\AjaxResponse|array + * The form render array or an AJAX response object. + */ + public function updateFormCallback(array &$form, FormStateInterface $form_state) { + // When the source field input contains errors, replace the existing form to + // let the user change the source field input. If the user input is valid, + // the entire modal is replaced with the second step of the form to show the + // form fields for each media item. + if ($form_state::hasAnyErrors()) { + $response = new AjaxResponse(); + $response->addCommand(new ReplaceCommand('#media-library-add-form-wrapper', $form)); + return $response; + } + + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + $added_media = $form_state->get('media') ?: []; + foreach ($added_media as $delta => $media) { + $this->validateMediaEntity($media, $form, $form_state, $delta); + } + } + + /** + * Validate a created media item. + * + * @param \Drupal\media\MediaInterface $media + * The media item to validate. + * @param array $form + * The complete form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current form state. + * @param int $delta + * The delta of the media item. + */ + protected function validateMediaEntity(MediaInterface $media, array $form, FormStateInterface $form_state, $delta) { + $form_display = EntityFormDisplay::collectRenderDisplay($media, 'media_library'); + $form_display->extractFormValues($media, $form['media'][$delta]['fields'], $form_state); + $form_display->validateFormValues($media, $form['media'][$delta]['fields'], $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $added_media = $form_state->get('media') ?: []; + foreach ($added_media as $delta => $media) { + EntityFormDisplay::collectRenderDisplay($media, 'media_library') + ->extractFormValues($media, $form['media'][$delta]['fields'], $form_state); + $this->prepareMediaEntityForSave($media); + $media->save(); + } + } + + /** + * AJAX callback to send the new media item(s) to the calling code. + * + * @param array $form + * The complete form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current form state. + * + * @return array|\Drupal\Core\Ajax\AjaxResponse + * The form array when there are form errors or a AJAX response to select + * the created items in the media library. + */ + public function updateWidget(array &$form, FormStateInterface $form_state) { + if ($form_state::hasAnyErrors()) { + return $form; + } + + $added_media = $form_state->get('media') ?: []; + $media_ids = array_map(function (MediaInterface $media) { + return $media->id(); + }, $added_media); + + // Get the render array for the media library. The media library state might + // contain the 'media_library_content' when it has been opened from a + // vertical tab. We need to remove that to make sure the render array + // contains the vertical tabs. Besides that, we also need to force the media + // library to create a new instance of the media add form. + // @see \Drupal\media_library\MediaLibraryUiBuilder::buildMediaTypeAddForm() + $state = MediaLibraryState::fromRequest($this->getRequest()); + $state->remove('media_library_content'); + $state->set('_media_library_form_rebuild', TRUE); + $library_ui = $this->libraryUiBuilder->buildUi($state); + + $response = new AjaxResponse(); + $response->addCommand(new UpdateSelectionCommand($media_ids)); + $response->addCommand(new ReplaceCommand('#media-library-add-form-wrapper', $library_ui)); + return $response; + } + + /** + * Returns the name of the source field for a media type. + * + * @param \Drupal\media\MediaTypeInterface $media_type + * The media type to get the source field name for. + * + * @return string + * The name of the media type's source field. + */ + protected function getSourceFieldName(MediaTypeInterface $media_type) { + return $media_type->getSource() + ->getSourceFieldDefinition($media_type) + ->getName(); + } + +} diff --git a/core/modules/media_library/src/Form/FileUploadForm.php b/core/modules/media_library/src/Form/FileUploadForm.php new file mode 100644 index 000000000000..e89b67e7d150 --- /dev/null +++ b/core/modules/media_library/src/Form/FileUploadForm.php @@ -0,0 +1,248 @@ +<?php + +namespace Drupal\media_library\Form; + +use Drupal\Core\Entity\EntityStorageInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Field\FieldStorageDefinitionInterface; +use Drupal\Core\Field\TypedData\FieldItemDataDefinition; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\ElementInfoManagerInterface; +use Drupal\Core\Render\RendererInterface; +use Drupal\file\FileInterface; +use Drupal\file\Plugin\Field\FieldType\FileFieldItemList; +use Drupal\file\Plugin\Field\FieldType\FileItem; +use Drupal\media\MediaInterface; +use Drupal\media\MediaTypeInterface; +use Drupal\media_library\MediaLibraryUiBuilder; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Creates a form to create media entities from uploaded files. + * + * @internal + * Media Library is an experimental module and its internal code may be + * subject to change in minor releases. External code should not instantiate + * or extend this class. + */ +class FileUploadForm extends AddFormBase { + + /** + * The element info manager. + * + * @var \Drupal\Core\Render\ElementInfoManagerInterface + */ + protected $elementInfo; + + /** + * The renderer service. + * + * @var \Drupal\Core\Render\ElementInfoManagerInterface + */ + protected $renderer; + + /** + * Constructs a new FileUploadForm. + * + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + * @param \Drupal\media_library\MediaLibraryUiBuilder $library_ui_builder + * The media library UI builder. + * @param \Drupal\Core\Render\ElementInfoManagerInterface $element_info + * The element info manager. + * @param \Drupal\Core\Render\RendererInterface $renderer + * The renderer service. + */ + public function __construct(EntityTypeManagerInterface $entity_type_manager, MediaLibraryUiBuilder $library_ui_builder, ElementInfoManagerInterface $element_info, RendererInterface $renderer) { + parent::__construct($entity_type_manager, $library_ui_builder); + $this->elementInfo = $element_info; + $this->renderer = $renderer; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_type.manager'), + $container->get('media_library.ui_builder'), + $container->get('element_info'), + $container->get('renderer') + ); + } + + /** + * {@inheritdoc} + */ + protected function getMediaType(FormStateInterface $form_state) { + if ($this->mediaType) { + return $this->mediaType; + } + + $media_type = parent::getMediaType($form_state); + // The file upload form only supports media types which use a file field as + // a source field. + $field_definition = $media_type->getSource()->getSourceFieldDefinition($media_type); + if (!is_a($field_definition->getClass(), FileFieldItemList::class, TRUE)) { + throw new \InvalidArgumentException('Can only add media types which use a file field as a source field.'); + } + return $media_type; + } + + /** + * {@inheritdoc} + */ + protected function buildInputElement(array $form, FormStateInterface $form_state) { + $form['#attributes']['class'][] = 'media-library-add-form-upload'; + + // Create a file item to get the upload validators. + $media_type = $this->getMediaType($form_state); + $item = $this->createFileItem($media_type); + + /** @var \Drupal\media_library\MediaLibraryState $state */ + $state = $form_state->get('media_library_state'); + if (!$state->hasSlotsAvailable()) { + return $form; + } + + $slots = $state->getAvailableSlots(); + + $process = (array) $this->elementInfo->getInfoProperty('managed_file', '#process', []); + $form['upload'] = [ + '#type' => 'managed_file', + '#title' => $this->formatPlural($slots, 'Add file', 'Add files'), + // @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, + '#remaining_slots' => $slots, + ]; + + $file_upload_help = [ + '#theme' => 'file_upload_help', + '#upload_validators' => $form['upload']['#upload_validators'], + '#cardinality' => $slots, + ]; + + // The file upload help needs to be rendered since the description does not + // accept render arrays. The FileWidget::formElement() method adds the file + // upload help in the same way, so any theming improvements made to file + // fields would also be applied to this upload field. + // @see \Drupal\file\Plugin\Field\FieldWidget\FileWidget::formElement() + $form['upload']['#description'] = $this->renderer->renderPlain($file_upload_help); + + return $form; + } + + /** + * Validates the upload element. + * + * @param array $element + * The upload element. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + * + * @return array + * The processed upload element. + */ + public function validateUploadElement(array $element, FormStateInterface $form_state) { + if ($form_state::hasAnyErrors()) { + // When an error occurs during uploading files, remove all files so the + // user can re-upload the files. + $element['#value'] = []; + } + $values = $form_state->getValue('upload', []); + if (count($values['fids']) > $element['#cardinality'] && $element['#cardinality'] !== FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) { + $form_state->setError($element, $this->t('A maximum of @count files can be uploaded.', [ + '@count' => $element['#cardinality'], + ])); + $form_state->setValue('upload', []); + $element['#value'] = []; + } + return $element; + } + + /** + * Processes an upload (managed_file) element. + * + * @param array $element + * The upload element. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + * + * @return array + * The processed upload element. + */ + public function processUploadElement(array $element, FormStateInterface $form_state) { + $element['upload_button']['#submit'] = ['::uploadButtonSubmit']; + $element['upload_button']['#ajax'] = [ + 'callback' => '::updateFormCallback', + 'wrapper' => 'media-library-wrapper', + ]; + return $element; + } + + /** + * Submit handler for the upload button, inside the managed_file element. + * + * @param array $form + * The form render array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + */ + public function uploadButtonSubmit(array $form, FormStateInterface $form_state) { + $files = $this->entityTypeManager + ->getStorage('file') + ->loadMultiple($form_state->getValue('upload', [])); + $this->processInputValues($files, $form, $form_state); + } + + /** + * {@inheritdoc} + */ + protected function createMediaFromValue(MediaTypeInterface $media_type, EntityStorageInterface $media_storage, $source_field_name, $file) { + if (!($file instanceof FileInterface)) { + throw new \InvalidArgumentException('Cannot create a media item without a file entity.'); + } + + // Create a file item to get the upload location. + $item = $this->createFileItem($media_type); + $upload_location = $item->getUploadLocation(); + if (!file_prepare_directory($upload_location, FILE_CREATE_DIRECTORY)) { + throw new \Exception("The destination directory '$upload_location' is not writable"); + } + $file = file_move($file, $upload_location); + if (!$file) { + throw new \RuntimeException("Unable to move file to '$upload_location'"); + } + + return parent::createMediaFromValue($media_type, $media_storage, $source_field_name, $file)->setName($file->getFilename()); + } + + /** + * Create a file field item. + * + * @param \Drupal\media\MediaTypeInterface $media_type + * The media type of the media item. + * + * @return \Drupal\file\Plugin\Field\FieldType\FileItem + * A created file item. + */ + protected function createFileItem(MediaTypeInterface $media_type) { + $field_definition = $media_type->getSource()->getSourceFieldDefinition($media_type); + $data_definition = FieldItemDataDefinition::create($field_definition); + return new FileItem($data_definition); + } + + /** + * {@inheritdoc} + */ + protected function prepareMediaEntityForSave(MediaInterface $media) { + /** @var \Drupal\file\FileInterface $file */ + $file = $media->get($this->getSourceFieldName($media->bundle->entity))->entity; + $file->setPermanent(); + $file->save(); + } + +} diff --git a/core/modules/media_library/src/Form/MediaLibraryUploadForm.php b/core/modules/media_library/src/Form/MediaLibraryUploadForm.php deleted file mode 100644 index 66670ffe1bcd..000000000000 --- a/core/modules/media_library/src/Form/MediaLibraryUploadForm.php +++ /dev/null @@ -1,639 +0,0 @@ -<?php - -namespace Drupal\media_library\Form; - -use Drupal\Core\Access\AccessResultAllowed; -use Drupal\Core\Ajax\AjaxResponse; -use Drupal\Core\Ajax\CloseDialogCommand; -use Drupal\Core\Ajax\InvokeCommand; -use Drupal\Core\Entity\Entity\EntityFormDisplay; -use Drupal\Core\Entity\EntityTypeManagerInterface; -use Drupal\Core\Field\FieldStorageDefinitionInterface; -use Drupal\Core\Field\TypedData\FieldItemDataDefinition; -use Drupal\Core\Form\FormBase; -use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Render\ElementInfoManagerInterface; -use Drupal\file\FileInterface; -use Drupal\file\Plugin\Field\FieldType\FileFieldItemList; -use Drupal\file\Plugin\Field\FieldType\FileItem; -use Drupal\media\MediaInterface; -use Drupal\media\MediaTypeInterface; -use Drupal\media_library\MediaLibraryState; -use Drupal\media_library\Plugin\Field\FieldWidget\MediaLibraryWidget; -use Symfony\Component\DependencyInjection\ContainerInterface; -use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; - -/** - * Creates a form to create media entities from uploaded files. - * - * @internal - */ -class MediaLibraryUploadForm extends FormBase { - - /** - * The element info manager. - * - * @var \Drupal\Core\Render\ElementInfoManagerInterface - */ - protected $elementInfo; - - /** - * The entity type manager. - * - * @var \Drupal\Core\Entity\EntityTypeManagerInterface - */ - protected $entityTypeManager; - - /** - * Media types the current user has access to. - * - * @var \Drupal\media\MediaTypeInterface[] - */ - protected $types; - - /** - * The media being processed. - * - * @var \Drupal\media\MediaInterface[] - */ - protected $media = []; - - /** - * The files waiting for type selection. - * - * @var \Drupal\file\FileInterface[] - */ - protected $files = []; - - /** - * Indicates whether the 'medium' image style exists. - * - * @var bool - */ - protected $mediumStyleExists = FALSE; - - /** - * Constructs a new MediaLibraryUploadForm. - * - * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager - * The entity type manager. - * @param \Drupal\Core\Render\ElementInfoManagerInterface $element_info - * The element info manager. - */ - public function __construct(EntityTypeManagerInterface $entity_type_manager, ElementInfoManagerInterface $element_info) { - $this->entityTypeManager = $entity_type_manager; - $this->elementInfo = $element_info; - $this->mediumStyleExists = !empty($entity_type_manager->getStorage('image_style')->load('medium')); - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container) { - return new static( - $container->get('entity_type.manager'), - $container->get('element_info') - ); - } - - /** - * {@inheritdoc} - */ - public function getFormId() { - return 'media_library_upload_form'; - } - - /** - * {@inheritdoc} - */ - public function buildForm(array $form, FormStateInterface $form_state) { - $form['#prefix'] = '<div id="media-library-upload-wrapper">'; - $form['#suffix'] = '</div>'; - - $form['#attached']['library'][] = 'media_library/style'; - - $form['#attributes']['class'][] = 'media-library-upload'; - - if (empty($this->media) && empty($this->files)) { - $process = (array) $this->elementInfo->getInfoProperty('managed_file', '#process', []); - $upload_validators = $this->mergeUploadValidators($this->getTypes()); - $form['upload'] = [ - '#type' => 'managed_file', - '#title' => $this->t('Upload'), - // @todo Move validation in https://www.drupal.org/node/2988215 - '#process' => array_merge(['::validateUploadElement'], $process, ['::processUploadElement']), - '#upload_validators' => $upload_validators, - ]; - $form['upload_help'] = [ - '#theme' => 'file_upload_help', - '#description' => $this->t('Upload files here to add new media.'), - '#upload_validators' => $upload_validators, - ]; - $remaining = (int) $this->getRequest()->query->get('media_library_remaining'); - if ($remaining || $remaining === FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) { - $form['upload']['#multiple'] = $remaining > 1 || $remaining === FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED; - $form['upload']['#cardinality'] = $form['upload_help']['#cardinality'] = $remaining; - } - } - else { - $form['media'] = [ - '#type' => 'container', - ]; - foreach ($this->media as $i => $media) { - $source_field = $media->getSource() - ->getSourceFieldDefinition($media->bundle->entity) - ->getName(); - - $element = [ - '#type' => 'container', - '#attributes' => [ - 'class' => [ - 'media-library-upload__media', - ], - ], - 'preview' => [ - '#type' => 'container', - '#attributes' => [ - 'class' => [ - 'media-library-upload__media-preview', - ], - ], - ], - 'fields' => [ - '#type' => 'container', - '#attributes' => [ - 'class' => [ - 'media-library-upload__media-fields', - ], - ], - // Parents is set here as it is used in the form display. - '#parents' => ['media', $i, 'fields'], - ], - ]; - // @todo Make this configurable in https://www.drupal.org/node/2988223 - if ($this->mediumStyleExists && $thumbnail_uri = $media->getSource()->getMetadata($media, 'thumbnail_uri')) { - $element['preview']['thumbnail'] = [ - '#theme' => 'image_style', - '#style_name' => 'medium', - '#uri' => $thumbnail_uri, - ]; - } - - $form_display = EntityFormDisplay::collectRenderDisplay($media, 'media_library'); - // When the name is not added to the form as a editable field, output - // the name as a fixed element to confirm the right file was uploaded. - if (!$form_display->getComponent('name')) { - $element['fields']['name'] = [ - '#type' => 'item', - '#title' => $this->t('Name'), - '#markup' => $media->getName(), - ]; - } - $form_display->buildForm($media, $element['fields'], $form_state); - - // We hide certain elements in the image widget with CSS. - if (isset($element['fields'][$source_field])) { - $element['fields'][$source_field]['#attributes']['class'][] = 'media-library-upload__source-field'; - } - if (isset($element['fields']['revision_log_message'])) { - $element['fields']['revision_log_message']['#access'] = FALSE; - } - $form['media'][$i] = $element; - } - - $form['files'] = [ - '#type' => 'container', - ]; - foreach ($this->files as $i => $file) { - $types = $this->filterTypesThatAcceptFile($file, $this->getTypes()); - $form['files'][$i] = [ - '#type' => 'container', - '#attributes' => [ - 'class' => [ - 'media-library-upload__file', - ], - ], - 'help' => [ - '#markup' => '<strong class="media-library-upload__file-label">' . $this->t('Select a media type for %filename:', [ - '%filename' => $file->getFilename(), - ]) . '</strong>', - ], - ]; - foreach ($types as $type) { - $form['files'][$i][$type->id()] = [ - '#type' => 'submit', - '#media_library_index' => $i, - '#media_library_type' => $type->id(), - '#value' => $type->label(), - '#submit' => ['::selectType'], - '#ajax' => [ - 'callback' => '::updateFormCallback', - 'wrapper' => 'media-library-upload-wrapper', - ], - '#limit_validation_errors' => [['files', $i, $type->id()]], - ]; - } - } - - $form['actions'] = [ - '#type' => 'actions', - ]; - $form['actions']['submit'] = [ - '#type' => 'submit', - '#value' => $this->t('Save'), - '#ajax' => [ - 'callback' => '::updateWidget', - 'wrapper' => 'media-library-upload-wrapper', - ], - ]; - } - - return $form; - } - - /** - * {@inheritdoc} - */ - public function validateForm(array &$form, FormStateInterface $form_state) { - if (count($this->files)) { - $form_state->setError($form['files'], $this->t('Please select a media type for all files.')); - } - foreach ($this->media as $i => $media) { - $form_display = EntityFormDisplay::collectRenderDisplay($media, 'media_library'); - $form_display->extractFormValues($media, $form['media'][$i]['fields'], $form_state); - $form_display->validateFormValues($media, $form['media'][$i]['fields'], $form_state); - } - } - - /** - * {@inheritdoc} - */ - public function submitForm(array &$form, FormStateInterface $form_state) { - foreach ($this->media as $i => $media) { - EntityFormDisplay::collectRenderDisplay($media, 'media_library') - ->extractFormValues($media, $form['media'][$i]['fields'], $form_state); - $source_field = $media->getSource()->getSourceFieldDefinition($media->bundle->entity)->getName(); - /** @var \Drupal\file\FileInterface $file */ - $file = $media->get($source_field)->entity; - $file->setPermanent(); - $file->save(); - $media->save(); - } - } - - /** - * AJAX callback to select a media type for a file. - * - * @param array $form - * An associative array containing the structure of the form. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the form. - * - * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException - * If the triggering element is missing required properties. - */ - public function selectType(array &$form, FormStateInterface $form_state) { - $element = $form_state->getTriggeringElement(); - if (!isset($element['#media_library_index']) || !isset($element['#media_library_type'])) { - throw new BadRequestHttpException('The "#media_library_index" and "#media_library_type" properties on the triggering element are required for type selection.'); - } - $i = $element['#media_library_index']; - $type = $element['#media_library_type']; - $this->media[] = $this->createMediaEntity($this->files[$i], $this->getTypes()[$type]); - unset($this->files[$i]); - $form_state->setRebuild(); - } - - /** - * AJAX callback to update the field widget. - * - * @param array $form - * An associative array containing the structure of the form. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the form. - * - * @return \Drupal\Core\Ajax\AjaxResponse - * A command to send the selection to the current field widget. - */ - public function updateWidget(array &$form, FormStateInterface $form_state) { - if ($form_state->getErrors()) { - return $form; - } - - $mids = array_map(function (MediaInterface $media) { - return $media->id(); - }, $this->media); - - // Pass the selection to the field widget based on the current widget ID. - $opener_id = MediaLibraryState::fromRequest($this->getRequest())->getOpenerId(); - if ($field_id = MediaLibraryWidget::getOpenerFieldId($opener_id)) { - return (new AjaxResponse()) - ->addCommand(new InvokeCommand("[data-media-library-widget-value=\"$field_id\"]", 'val', [implode(',', $mids)])) - ->addCommand(new InvokeCommand("[data-media-library-widget-update=\"$field_id\"]", 'trigger', ['mousedown'])) - ->addCommand(new CloseDialogCommand()); - } - } - - /** - * Processes an upload (managed_file) element. - * - * @param array $element - * The upload element. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The form state. - * - * @return array - * The processed upload element. - */ - public function processUploadElement(array $element, FormStateInterface $form_state) { - $element['upload_button']['#submit'] = ['::uploadButtonSubmit']; - $element['upload_button']['#ajax'] = [ - 'callback' => '::updateFormCallback', - 'wrapper' => 'media-library-upload-wrapper', - ]; - return $element; - } - - /** - * Validates the upload element. - * - * @param array $element - * The upload element. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The form state. - * - * @return array - * The processed upload element. - */ - public function validateUploadElement(array $element, FormStateInterface $form_state) { - if ($form_state->getErrors()) { - $element['#value'] = []; - } - $values = $form_state->getValue('upload', []); - if (count($values['fids']) > $element['#cardinality'] && $element['#cardinality'] !== FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) { - $form_state->setError($element, $this->t('A maximum of @count files can be uploaded.', [ - '@count' => $element['#cardinality'], - ])); - $form_state->setValue('upload', []); - $element['#value'] = []; - } - return $element; - } - - /** - * Submit handler for the upload button, inside the managed_file element. - * - * @param array $form - * The form render array. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The form state. - */ - public function uploadButtonSubmit(array $form, FormStateInterface $form_state) { - $fids = $form_state->getValue('upload', []); - $files = $this->entityTypeManager->getStorage('file')->loadMultiple($fids); - /** @var \Drupal\file\FileInterface $file */ - foreach ($files as $file) { - $types = $this->filterTypesThatAcceptFile($file, $this->getTypes()); - if (!empty($types)) { - if (count($types) === 1) { - $this->media[] = $this->createMediaEntity($file, reset($types)); - } - else { - $this->files[] = $file; - } - } - } - $form_state->setRebuild(); - } - - /** - * Creates a new, unsaved media entity. - * - * @param \Drupal\file\FileInterface $file - * A file for the media source field. - * @param \Drupal\media\MediaTypeInterface $type - * A media type. - * - * @return \Drupal\media\MediaInterface - * An unsaved media entity. - * - * @throws \Exception - * If a file operation failed when moving the upload. - */ - protected function createMediaEntity(FileInterface $file, MediaTypeInterface $type) { - $media = $this->entityTypeManager->getStorage('media')->create([ - 'bundle' => $type->id(), - 'name' => $file->getFilename(), - ]); - $source_field = $type->getSource()->getSourceFieldDefinition($type)->getName(); - $location = $this->getUploadLocationForType($media->bundle->entity); - if (!file_prepare_directory($location, FILE_CREATE_DIRECTORY)) { - throw new \Exception("The destination directory '$location' is not writable"); - } - $file = file_move($file, $location); - if (!$file) { - throw new \Exception("Unable to move file to '$location'"); - } - $media->set($source_field, $file->id()); - return $media; - } - - /** - * AJAX callback for refreshing the entire form. - * - * @param array $form - * The form render array. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The form state. - * - * @return array - * The form render array. - */ - public function updateFormCallback(array &$form, FormStateInterface $form_state) { - return $form; - } - - /** - * Access callback to check that the user can create file based media. - * - * @param array $allowed_types - * (optional) The contextually allowed types. - * - * @return \Drupal\Core\Access\AccessResultInterface - * The access result. - * - * @todo Remove $allowed_types param in https://www.drupal.org/node/2956747 - */ - public function access(array $allowed_types = NULL) { - return AccessResultAllowed::allowedIf(count($this->getTypes($allowed_types)))->mergeCacheMaxAge(0); - } - - /** - * Returns media types which use files that the current user can create. - * - * @param array $allowed_types - * (optional) The contextually allowed types. - * - * @todo Move in https://www.drupal.org/node/2987924 - * - * @return \Drupal\media\MediaTypeInterface[] - * A list of media types that are valid for this form. - */ - protected function getTypes(array $allowed_types = NULL) { - // Cache results if possible. - if (!isset($this->types)) { - $media_type_storage = $this->entityTypeManager->getStorage('media_type'); - if (!$allowed_types) { - $types = $media_type_storage->loadMultiple(MediaLibraryState::fromRequest($this->getRequest())->getAllowedTypeIds()); - } - else { - $types = $media_type_storage->loadMultiple($allowed_types); - } - $types = $this->filterTypesWithFileSource($types); - $types = $this->filterTypesWithCreateAccess($types); - $this->types = $types; - } - return $this->types; - } - - /** - * Filters media types that accept a given file. - * - * @todo Move in https://www.drupal.org/node/2987924 - * - * @param \Drupal\file\FileInterface $file - * A file entity. - * @param \Drupal\media\MediaTypeInterface[] $types - * An array of available media types. - * - * @return \Drupal\media\MediaTypeInterface[] - * An array of media types that accept the file. - */ - protected function filterTypesThatAcceptFile(FileInterface $file, array $types) { - $types = $this->filterTypesWithFileSource($types); - return array_filter($types, function (MediaTypeInterface $type) use ($file) { - $validators = $this->getUploadValidatorsForType($type); - $errors = file_validate($file, $validators); - return empty($errors); - }); - } - - /** - * Filters an array of media types that accept file sources. - * - * @todo Move in https://www.drupal.org/node/2987924 - * - * @param \Drupal\media\MediaTypeInterface[] $types - * An array of media types. - * - * @return \Drupal\media\MediaTypeInterface[] - * An array of media types that accept file sources. - */ - protected function filterTypesWithFileSource(array $types) { - return array_filter($types, function (MediaTypeInterface $type) { - return is_a($type->getSource()->getSourceFieldDefinition($type)->getClass(), FileFieldItemList::class, TRUE); - }); - } - - /** - * Merges file upload validators for an array of media types. - * - * @todo Move in https://www.drupal.org/node/2987924 - * - * @param \Drupal\media\MediaTypeInterface[] $types - * An array of media types. - * - * @return array - * An array suitable for passing to file_save_upload() or the file field - * element's '#upload_validators' property. - */ - protected function mergeUploadValidators(array $types) { - $max_size = 0; - $extensions = []; - $types = $this->filterTypesWithFileSource($types); - foreach ($types as $type) { - $validators = $this->getUploadValidatorsForType($type); - if (isset($validators['file_validate_size'])) { - $max_size = max($max_size, $validators['file_validate_size'][0]); - } - if (isset($validators['file_validate_extensions'])) { - $extensions = array_unique(array_merge($extensions, explode(' ', $validators['file_validate_extensions'][0]))); - } - } - // If no field defines a max size, default to the system wide setting. - if ($max_size === 0) { - $max_size = file_upload_max_size(); - } - return [ - 'file_validate_extensions' => [implode(' ', $extensions)], - 'file_validate_size' => [$max_size], - ]; - } - - /** - * Gets upload validators for a given media type. - * - * @todo Move in https://www.drupal.org/node/2987924 - * - * @param \Drupal\media\MediaTypeInterface $type - * A media type. - * - * @return array - * An array suitable for passing to file_save_upload() or the file field - * element's '#upload_validators' property. - */ - protected function getUploadValidatorsForType(MediaTypeInterface $type) { - return $this->getFileItemForType($type)->getUploadValidators(); - } - - /** - * Gets upload destination for a given media type. - * - * @todo Move in https://www.drupal.org/node/2987924 - * - * @param \Drupal\media\MediaTypeInterface $type - * A media type. - * - * @return string - * An unsanitized file directory URI with tokens replaced. - */ - protected function getUploadLocationForType(MediaTypeInterface $type) { - return $this->getFileItemForType($type)->getUploadLocation(); - } - - /** - * Creates a file item for a given media type. - * - * @todo Move in https://www.drupal.org/node/2987924 - * - * @param \Drupal\media\MediaTypeInterface $type - * A media type. - * - * @return \Drupal\file\Plugin\Field\FieldType\FileItem - * The file item. - */ - protected function getFileItemForType(MediaTypeInterface $type) { - $source = $type->getSource(); - $source_data_definition = FieldItemDataDefinition::create($source->getSourceFieldDefinition($type)); - return new FileItem($source_data_definition); - } - - /** - * Filters an array of media types that can be created by the current user. - * - * @todo Move in https://www.drupal.org/node/2987924 - * - * @param \Drupal\media\MediaTypeInterface[] $types - * An array of media types. - * - * @return \Drupal\media\MediaTypeInterface[] - * An array of media types that accept file sources. - */ - protected function filterTypesWithCreateAccess(array $types) { - $access_handler = $this->entityTypeManager->getAccessControlHandler('media'); - return array_filter($types, function (MediaTypeInterface $type) use ($access_handler) { - return $access_handler->createAccess($type->id()); - }); - } - -} diff --git a/core/modules/media_library/src/MediaLibraryState.php b/core/modules/media_library/src/MediaLibraryState.php index ea740dba8003..018c42a74c36 100644 --- a/core/modules/media_library/src/MediaLibraryState.php +++ b/core/modules/media_library/src/MediaLibraryState.php @@ -29,8 +29,9 @@ * items can be selected. * * @internal - * This class is an internal part of the media library and should not be - * instantiated or used by external code. + * Media Library is an experimental module and its internal code may be + * subject to change in minor releases. External code should not instantiate + * or extend this class. */ class MediaLibraryState extends ParameterBag { diff --git a/core/modules/media_library/src/MediaLibraryUiBuilder.php b/core/modules/media_library/src/MediaLibraryUiBuilder.php index c97cc33764ad..4359e0b0e4e2 100644 --- a/core/modules/media_library/src/MediaLibraryUiBuilder.php +++ b/core/modules/media_library/src/MediaLibraryUiBuilder.php @@ -3,6 +3,8 @@ namespace Drupal\media_library; use Drupal\Core\Access\AccessResult; +use Drupal\Core\Form\FormBuilderInterface; +use Drupal\Core\Form\FormState; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\EventSubscriber\MainContentViewSubscriber; use Drupal\Core\Session\AccountInterface; @@ -15,13 +17,21 @@ * Service which builds the media library. * * @internal - * This class is an internal part of the media library and should not be - * instantiated or used by external code. + * Media Library is an experimental module and its internal code may be + * subject to change in minor releases. External code should not instantiate + * or extend this class. */ class MediaLibraryUiBuilder { use StringTranslationTrait; + /** + * The form builder. + * + * @var \Drupal\Core\Form\FormBuilderInterface + */ + protected $formBuilder; + /** * The entity type manager. * @@ -52,11 +62,14 @@ class MediaLibraryUiBuilder { * The request stack. * @param \Drupal\views\ViewExecutableFactory $views_executable_factory * The views executable factory. + * @param \Drupal\Core\Form\FormBuilderInterface $form_builder + * The currently active request object. */ - public function __construct(EntityTypeManagerInterface $entity_type_manager, RequestStack $request_stack, ViewExecutableFactory $views_executable_factory) { + public function __construct(EntityTypeManagerInterface $entity_type_manager, RequestStack $request_stack, ViewExecutableFactory $views_executable_factory, FormBuilderInterface $form_builder) { $this->entityTypeManager = $entity_type_manager; $this->request = $request_stack->getCurrentRequest(); $this->viewsExecutableFactory = $views_executable_factory; + $this->formBuilder = $form_builder; } /** @@ -77,11 +90,17 @@ public static function dialogOptions() { /** * Build the media library UI. * + * @param \Drupal\media_library\MediaLibraryState $state + * (optional) The current state of the media library, derived from the + * current request. + * * @return array * The render array for the media library. */ - public function buildUi() { - $state = MediaLibraryState::fromRequest($this->request); + public function buildUi(MediaLibraryState $state = NULL) { + if (!$state) { + $state = MediaLibraryState::fromRequest($this->request); + } // When navigating to a media type through the vertical tabs, we only want // to load the changed library content. This is not only more efficient, but // also provides a more accessible user experience for screen readers. @@ -123,6 +142,7 @@ protected function buildLibraryContent(MediaLibraryState $state) { 'class' => ['media-library-content'], 'tabindex' => -1, ], + 'form' => $this->buildMediaTypeAddForm($state), 'view' => $this->buildMediaLibraryView($state), ]; } @@ -178,13 +198,15 @@ protected function buildMediaTypeMenu(MediaLibraryState $state) { ], ]; - // Get the state parameters but remove the wrapper format. Also add the - // 'media_library_content' argument to fetch only the updated content for - // the tab. - // @see self::buildUi() - $state->remove(MainContentViewSubscriber::WRAPPER_FORMAT); - $state->add(['media_library_content' => 1]); + // Get the state parameters but remove the wrapper format, AJAX form and + // form rebuild parameters. These are internal parameters that should never + // be part of the vertical tab links. $query = $state->all(); + unset($query[MainContentViewSubscriber::WRAPPER_FORMAT], $query[FormBuilderInterface::AJAX_FORM_REQUEST], $query['_media_library_form_rebuild']); + // Add the 'media_library_content' parameter so the response will contain + // only the updated content for the tab. + // @see self::buildUi() + $query['media_library_content'] = 1; $allowed_types = $this->entityTypeManager->getStorage('media_type')->loadMultiple($allowed_type_ids); @@ -216,6 +238,42 @@ protected function buildMediaTypeMenu(MediaLibraryState $state) { return $menu; } + /** + * Get the add form for the selected media type. + * + * @param \Drupal\media_library\MediaLibraryState $state + * The current state of the media library, derived from the current request. + * + * @return array + * The render array for the media type add form. + */ + protected function buildMediaTypeAddForm(MediaLibraryState $state) { + $selected_type_id = $state->getSelectedTypeId(); + + if (!$this->entityTypeManager->getAccessControlHandler('media')->createAccess($selected_type_id)) { + return []; + } + + $selected_type = $this->entityTypeManager->getStorage('media_type')->load($selected_type_id); + $plugin_definition = $selected_type->getSource()->getPluginDefinition(); + + if (empty($plugin_definition['forms']['media_library_add'])) { + return []; + } + + // After the form to add new media is submitted, we need to rebuild the + // media library with a new instance of the media add form. The form API + // allows us to do that by forcing empty user input. + // @see \Drupal\Core\Form\FormBuilder::doBuildForm() + $form_state = new FormState(); + if ($state->get('_media_library_form_rebuild')) { + $form_state->setUserInput([]); + $state->remove('_media_library_form_rebuild'); + } + $form_state->set('media_library_state', $state); + return $this->formBuilder->buildForm($plugin_definition['forms']['media_library_add'], $form_state); + } + /** * Get the media library view. * 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 033c7da139ff..3ecf3ed78e08 100644 --- a/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php @@ -33,6 +33,9 @@ * ) * * @internal + * Media Library is an experimental module and its internal code may be + * subject to change in minor releases. External code should not instantiate + * or extend this class. */ class MediaLibraryWidget extends WidgetBase implements ContainerFactoryPluginInterface { 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 d1d6b4cad634..5660e43f4a73 100644 --- a/core/modules/media_library/src/Plugin/views/field/MediaLibrarySelectForm.php +++ b/core/modules/media_library/src/Plugin/views/field/MediaLibrarySelectForm.php @@ -20,6 +20,9 @@ * @ViewsField("media_library_select_form") * * @internal + * Media Library is an experimental module and its internal code may be + * subject to change in minor releases. External code should not instantiate + * or extend this class. */ class MediaLibrarySelectForm extends FieldPluginBase { diff --git a/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php b/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php index 64f3ff59b3a1..34245daec406 100644 --- a/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php +++ b/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php @@ -150,8 +150,8 @@ public function testWidgetAccess() { $role->save(); // Create a working state. - $allowed_types = ['type_one', 'type_two']; - $state = MediaLibraryState::create('test', $allowed_types, 'type_two', 2); + $allowed_types = ['type_one', 'type_two', 'type_three', 'type_four']; + $state = MediaLibraryState::create('test', $allowed_types, 'type_three', 2); $url_options = ['query' => $state->all()]; // Verify that unprivileged users can't access the widget view. @@ -169,6 +169,18 @@ public function testWidgetAccess() { $assert_session->elementExists('css', '.view-media-library'); $this->drupalGet('media-library', $url_options); $assert_session->elementExists('css', '.view-media-library'); + // Assert the user does not have access to the media add form if the user + // does not have the 'create media' permission. + $assert_session->fieldNotExists('files[upload][]'); + + // Assert users with the 'create media' permission can access the media add + // form. + $this->grantPermissions($role, [ + 'create media', + ]); + $this->drupalGet('media-library', $url_options); + $assert_session->elementExists('css', '.view-media-library'); + $assert_session->fieldExists('Add files'); } /** @@ -258,7 +270,7 @@ public function testWidget() { $assert_session->pageTextContains('Dog'); $assert_session->pageTextContains('Bear'); $assert_session->pageTextNotContains('Turtle'); - $assert_session->elementExists('named', ['link', 'Type Three'])->click(); + $page->clickLink('Type Three'); $assert_session->assertWaitOnAjaxRequest(); $assert_session->elementExists('named', ['link', 'Type Three (active tab)']); $assert_session->pageTextNotContains('Dog'); @@ -316,9 +328,9 @@ public function testWidget() { $this->assertFalse($checkboxes[3]->hasAttribute('disabled')); // The selection should be persisted when navigating to other media types in // the modal. - $assert_session->elementExists('named', ['link', 'Type Three'])->click(); + $page->clickLink('Type Three'); $assert_session->assertWaitOnAjaxRequest(); - $assert_session->elementExists('named', ['link', 'Type One'])->click(); + $page->clickLink('Type One'); $assert_session->assertWaitOnAjaxRequest(); $checkboxes = $page->findAll('css', '.media-library-view .js-click-to-select-checkbox input'); $selected_checkboxes = []; @@ -331,7 +343,7 @@ public function testWidget() { $assert_session->hiddenFieldValueEquals('media-library-modal-selection', implode(',', $selected_checkboxes)); $assert_session->elementTextContains('css', '.media-library-selected-count', '1 of 2 items selected'); // Add to selection from another type. - $assert_session->elementExists('named', ['link', 'Type Two'])->click(); + $page->clickLink('Type Two'); $assert_session->assertWaitOnAjaxRequest(); $checkboxes = $page->findAll('css', '.media-library-view .js-click-to-select-checkbox input'); $checkboxes[0]->click(); @@ -345,7 +357,7 @@ public function testWidget() { $this->assertTrue($checkboxes[2]->hasAttribute('disabled')); $this->assertTrue($checkboxes[3]->hasAttribute('disabled')); // Assert the checkboxes are also disabled on other pages. - $assert_session->elementExists('named', ['link', 'Type One'])->click(); + $page->clickLink('Type One'); $assert_session->assertWaitOnAjaxRequest(); $this->assertTrue($checkboxes[0]->hasAttribute('disabled')); $this->assertFalse($checkboxes[1]->hasAttribute('disabled')); @@ -473,6 +485,7 @@ public function testWidget() { */ public function testWidgetAnonymous() { $assert_session = $this->assertSession(); + $page = $this->getSession()->getPage(); $this->drupalLogout(); @@ -492,9 +505,7 @@ public function testWidgetAnonymous() { $assert_session->assertWaitOnAjaxRequest(); // Select the first media item (should be Dog). - $checkbox_selector = '.media-library-view .js-click-to-select-checkbox input'; - $checkboxes = $this->getSession()->getPage()->findAll('css', $checkbox_selector); - $checkboxes[0]->click(); + $page->find('css', '.media-library-view .js-click-to-select-checkbox input')->click(); $assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Select media'); $assert_session->assertWaitOnAjaxRequest(); @@ -533,6 +544,44 @@ public function testWidgetUpload() { $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_four media', + 'view media', + ]); + $this->drupalLogin($user); + + // Visit a node create page and open the media library. + $this->drupalGet('node/add/basic_page'); + $assert_session->elementExists('css', '.media-library-open-button[href*="field_twin_media"]')->click(); + $assert_session->assertWaitOnAjaxRequest(); + $assert_session->pageTextContains('Media library'); + + // Assert the upload form is visible for type_four. + $page->clickLink('Type Four'); + $assert_session->assertWaitOnAjaxRequest(); + $assert_session->fieldExists('Add files'); + $assert_session->pageTextContains('Maximum 2 files.'); + + // Assert the upload form is not visible for type_three. + $page->clickLink('Type Three'); + $assert_session->assertWaitOnAjaxRequest(); + $assert_session->fieldNotExists('files[upload][]'); + $assert_session->pageTextNotContains('Maximum 2 files.'); + + // Create a user that can create media for all media types. + $user = $this->drupalCreateUser([ + 'access administration pages', + 'access content', + 'create basic_page content', + 'create media', + 'view media', + ]); + $this->drupalLogin($user); + // Visit a node create page. $this->drupalGet('node/add/basic_page'); @@ -544,11 +593,19 @@ public function testWidgetUpload() { $assert_session->elementExists('css', '.media-library-open-button[href*="field_twin_media"]')->click(); $assert_session->assertWaitOnAjaxRequest(); $assert_session->pageTextContains('Media library'); - $assert_session->elementExists('css', '#drupal-modal')->clickLink('Add media'); - $assert_session->assertWaitOnAjaxRequest(); - $page->attachFileToField('Upload', $this->container->get('file_system')->realpath($png_image->uri)); + // Assert the default tab for media type one does not have an upload form. + $assert_session->fieldNotExists('files[upload][]'); + + // Assert we can upload a file to media type three. + $page->clickLink('Type Three'); $assert_session->assertWaitOnAjaxRequest(); + $assert_session->elementExists('css', '.media-library-add-form--without-input'); + $assert_session->elementNotExists('css', '.media-library-add-form--with-input'); + $page->attachFileToField('Add files', $this->container->get('file_system')->realpath($png_image->uri)); + $assert_session->assertWaitOnAjaxRequest(); + $assert_session->elementExists('css', '.media-library-add-form--with-input'); + $assert_session->elementNotExists('css', '.media-library-add-form--without-input'); // Files are temporary until the form is saved. $files = $file_storage->loadMultiple(); @@ -556,6 +613,11 @@ public function testWidgetUpload() { $this->assertSame('public://type-three-dir', $file_system->dirname($file->getFileUri())); $this->assertTrue($file->isTemporary()); + // Assert the revision_log_message field is not shown. + $upload_form = $assert_session->elementExists('css', '.media-library-add-form'); + $assert_session->fieldNotExists('Revision log message', $upload_form); + + // Assert the name field contains the filename and the alt text is required. $this->assertSame($assert_session->fieldExists('Name')->getValue(), $png_image->filename); $assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Save'); $assert_session->assertWaitOnAjaxRequest(); @@ -569,7 +631,23 @@ public function testWidgetUpload() { $file = array_pop($files); $this->assertFalse($file->isTemporary()); - // Ensure the media item was added. + // Load the created media item. + $media_storage = $this->container->get('entity_type.manager')->getStorage('media'); + $media_items = $media_storage->loadMultiple(); + $added_media = array_pop($media_items); + + // Ensure the media item was saved to the library and automatically + // selected. The added media items should be in the first position of the + // add form. + $assert_session->pageTextContains('Media library'); + $assert_session->pageTextContains($png_image->filename); + $assert_session->fieldValueEquals('media_library_select_form[0]', $added_media->id()); + $assert_session->checkboxChecked('media_library_select_form[0]'); + $assert_session->pageTextContains('1 of 2 items selected'); + + // Ensure the created item is added in the widget. + $assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Select media'); + $assert_session->assertWaitOnAjaxRequest(); $assert_session->pageTextNotContains('Media library'); $assert_session->pageTextContains($png_image->filename); @@ -577,52 +655,77 @@ public function testWidgetUpload() { $assert_session->elementExists('css', '.media-library-open-button[href*="field_unlimited_media"]')->click(); $assert_session->assertWaitOnAjaxRequest(); $assert_session->pageTextContains('Media library'); - $assert_session->elementExists('css', '#drupal-modal')->clickLink('Add media'); + + // Navigate to the media type three tab first. + $page->clickLink('Type Three'); $assert_session->assertWaitOnAjaxRequest(); + // Select a media item. + $page->find('css', '.media-library-view .js-click-to-select-checkbox input')->click(); + $assert_session->pageTextContains('1 item selected'); + // Multiple uploads should be allowed. // @todo Add test when https://github.com/minkphp/Mink/issues/358 is closed - $this->assertTrue($assert_session->fieldExists('Upload')->hasAttribute('multiple')); + $this->assertTrue($assert_session->fieldExists('Add files')->hasAttribute('multiple')); - $page->attachFileToField('Upload', $this->container->get('file_system')->realpath($png_image->uri)); + $page->attachFileToField('Add files', $this->container->get('file_system')->realpath($png_image->uri)); $assert_session->assertWaitOnAjaxRequest(); $page->fillField('Name', 'Unlimited Cardinality Image'); $page->fillField('Alternative text', $this->randomString()); $assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Save'); $assert_session->assertWaitOnAjaxRequest(); - // Ensure the media item was added. + // Load the created media item. + $media_storage = $this->container->get('entity_type.manager')->getStorage('media'); + $media_items = $media_storage->loadMultiple(); + $added_media = array_pop($media_items); + + // Ensure the media item was saved to the library and automatically + // selected. The added media items should be in the first position of the + // add form. + $assert_session->pageTextContains('Media library'); + $assert_session->pageTextContains('Unlimited Cardinality Image'); + $assert_session->fieldValueEquals('media_library_select_form[0]', $added_media->id()); + $assert_session->checkboxChecked('media_library_select_form[0]'); + + // Assert the item that was selected before uploading the file is still + // selected. + $assert_session->pageTextContains('2 items selected'); + $checkboxes = $page->findAll('css', '.media-library-view .js-click-to-select-checkbox input'); + $selected_checkboxes = []; + foreach ($checkboxes as $checkbox) { + if ($checkbox->isChecked()) { + $selected_checkboxes[] = $checkbox->getValue(); + } + } + $this->assertCount(2, $selected_checkboxes); + + // Ensure the created item is added in the widget. + $assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Select media'); + $assert_session->assertWaitOnAjaxRequest(); $assert_session->pageTextNotContains('Media library'); $assert_session->pageTextContains('Unlimited Cardinality Image'); - // Open the browser again to test type resolution. + // Verify we can only upload the files allowed by the media type. $assert_session->elementExists('css', '.media-library-open-button[href*="field_twin_media"]')->click(); $assert_session->assertWaitOnAjaxRequest(); $assert_session->pageTextContains('Media library'); - $assert_session->elementExists('css', '#drupal-modal')->clickLink('Add media'); + $page->clickLink('Type Four'); $assert_session->assertWaitOnAjaxRequest(); - $page->attachFileToField('Upload', $file_system->realpath($jpg_image->uri)); - $assert_session->assertWaitOnAjaxRequest(); - - $assert_session->pageTextContains('Select a media type for ' . $jpg_image->filename); - - // Before the type is determined, the file lives in the default upload - // location (temporary://). - $files = $file_storage->loadMultiple(); - $file = array_pop($files); - $this->assertSame('temporary', $file_system->uriScheme($file->getFileUri())); + // Assert we can now only upload one more media item. + $this->assertFalse($assert_session->fieldExists('Add file')->hasAttribute('multiple')); + $assert_session->pageTextContains('One file only.'); - // Both the type_three and type_four media types accept jpg images. - $assert_session->buttonExists('Type Three'); - $assert_session->buttonExists('Type Four')->click(); + // Assert media type four should only allow jpg files by trying a png file + // first. + $page->attachFileToField('Add file', $file_system->realpath($png_image->uri)); $assert_session->assertWaitOnAjaxRequest(); + $assert_session->pageTextContains('Only files with the following extensions are allowed'); - // The file should have been moved when the type was selected. - $files = $file_storage->loadMultiple(); - $file = array_pop($files); - $this->assertSame('public://type-four-dir', $file_system->dirname($file->getFileUri())); - $this->assertSame($assert_session->fieldExists('Name')->getValue(), $jpg_image->filename); + // Assert that jpg files are accepted by type four. + $page->attachFileToField('Add file', $file_system->realpath($jpg_image->uri)); + $assert_session->assertWaitOnAjaxRequest(); $page->fillField('Alternative text', $this->randomString()); // The type_four media type has another optional image field. @@ -637,6 +740,14 @@ public function testWidgetUpload() { $assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Save'); $assert_session->assertWaitOnAjaxRequest(); + // Ensure the media item was saved to the library and automatically + // selected. + $assert_session->pageTextContains('Media library'); + $assert_session->pageTextContains($jpg_image->filename); + + // Ensure the created item is added in the widget. + $assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Select media'); + $assert_session->assertWaitOnAjaxRequest(); $assert_session->pageTextNotContains('Media library'); $assert_session->pageTextContains($jpg_image->filename); } diff --git a/core/modules/media_library/tests/src/Kernel/MediaLibraryAddFormTest.php b/core/modules/media_library/tests/src/Kernel/MediaLibraryAddFormTest.php new file mode 100644 index 000000000000..78d12f50ff3e --- /dev/null +++ b/core/modules/media_library/tests/src/Kernel/MediaLibraryAddFormTest.php @@ -0,0 +1,111 @@ +<?php + +namespace Drupal\Tests\media_library\Kernel; + +use Drupal\Core\Form\FormState; +use Drupal\KernelTests\KernelTestBase; +use Drupal\media_library\Form\FileUploadForm; +use Drupal\media_library\MediaLibraryState; +use Drupal\Tests\media\Traits\MediaTypeCreationTrait; +use Drupal\Tests\user\Traits\UserCreationTrait; + +/** + * Tests the media library add form. + * + * @group media_library + */ +class MediaLibraryAddFormTest extends KernelTestBase { + + use MediaTypeCreationTrait; + use UserCreationTrait; + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'media', + 'media_library', + 'file', + 'field', + 'image', + 'system', + 'views', + 'user', + ]; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->installEntitySchema('user'); + $this->installEntitySchema('file'); + $this->installSchema('file', 'file_usage'); + $this->installSchema('system', ['sequences', 'key_value_expire']); + $this->installEntitySchema('media'); + $this->installConfig([ + 'field', + 'system', + 'file', + 'image', + 'media', + 'media_library', + ]); + + // Create an account with special UID 1. + $this->createUser([]); + + $this->createMediaType('image', ['id' => 'image']); + $this->createMediaType('oembed:video', ['id' => 'remote_video']); + } + + /** + * Tests the media library add form. + */ + public function testMediaTypeAddForm() { + $entity_type_manager = \Drupal::entityTypeManager(); + $image = $entity_type_manager->getStorage('media_type')->load('image'); + $remote_video = $entity_type_manager->getStorage('media_type')->load('remote_video'); + $image_source_definition = $image->getSource()->getPluginDefinition(); + $remote_video_source_definition = $remote_video->getSource()->getPluginDefinition(); + + // Assert the form class is added to the media source. + $this->assertSame(FileUploadForm::class, $image_source_definition['forms']['media_library_add']); + $this->assertArrayNotHasKey('media_library_add', $remote_video_source_definition['forms']); + + // Assert the media library UI does not contains the add form when the user + // does not have access. + $state = MediaLibraryState::create('test', ['image', 'remote_video'], 'image', -1); + $library_ui = \Drupal::service('media_library.ui_builder')->buildUi($state); + $this->assertEmpty($library_ui['content']['form']); + + // Create a user that has access to the media add form. + $this->setCurrentUser($this->createUser([ + 'create image media', + ])); + $library_ui = \Drupal::service('media_library.ui_builder')->buildUi($state); + $this->assertSame('managed_file', $library_ui['content']['form']['upload']['#type']); + } + + /** + * Tests the validation of the library state in the media library add form. + */ + public function testFormStateValidation() { + $form_state = new FormState(); + $this->setExpectedException(\InvalidArgumentException::class, 'The media library state is not present in the form state.'); + \Drupal::formBuilder()->buildForm(FileUploadForm::class, $form_state); + } + + /** + * Tests the validation of the selected type in the media library add form. + */ + public function testSelectedTypeValidation() { + $state = MediaLibraryState::create('test', ['image', 'remote_video', 'header_image'], 'header_image', -1); + $form_state = new FormState(); + $form_state->set('media_library_state', $state); + $this->setExpectedException(\InvalidArgumentException::class, "The 'header_image' media type does not exist."); + \Drupal::formBuilder()->buildForm(FileUploadForm::class, $form_state); + } + +} -- GitLab