Issue #3023802 by seanB, phenaproxima, lauriii, Pancho, larowlan, shaal, dww: Show media add form directly on media type tab in media library

8.7.x
Gábor Hojtsy 2019-02-18 11:37:04 +01:00
parent ab15b22f43
commit 792a14f5bb
17 changed files with 1165 additions and 747 deletions

View File

@ -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.
*

View File

@ -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 {

View File

@ -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.
*

View File

@ -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) {

View File

@ -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\Render\Element;
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().
*/

View File

@ -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:

View File

@ -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']

View File

@ -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,
];
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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());
});
}
}

View File

@ -29,8 +29,9 @@ use Symfony\Component\HttpFoundation\Request;
* 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 {

View File

@ -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 @@ use Symfony\Component\HttpFoundation\RequestStack;
* 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 @@ class MediaLibraryUiBuilder {
/**
* 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() {
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 @@ class MediaLibraryUiBuilder {
'class' => ['media-library-content'],
'tabindex' => -1,
],
'form' => $this->buildMediaTypeAddForm($state),
'view' => $this->buildMediaLibraryView($state),
];
}
@ -178,13 +198,15 @@ class MediaLibraryUiBuilder {
],
];
// 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 @@ class MediaLibraryUiBuilder {
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.
*

View File

@ -33,6 +33,9 @@ use Symfony\Component\Validator\ConstraintViolationInterface;
* )
*
* @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 {

View File

@ -20,6 +20,9 @@ use Drupal\views\ResultRow;
* @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 {

View File

@ -150,8 +150,8 @@ class MediaLibraryTest extends WebDriverTestBase {
$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 @@ class MediaLibraryTest extends WebDriverTestBase {
$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 @@ class MediaLibraryTest extends WebDriverTestBase {
$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 @@ class MediaLibraryTest extends WebDriverTestBase {
$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 @@ class MediaLibraryTest extends WebDriverTestBase {
$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 @@ class MediaLibraryTest extends WebDriverTestBase {
$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 @@ class MediaLibraryTest extends WebDriverTestBase {
*/
public function testWidgetAnonymous() {
$assert_session = $this->assertSession();
$page = $this->getSession()->getPage();
$this->drupalLogout();
@ -492,9 +505,7 @@ class MediaLibraryTest extends WebDriverTestBase {
$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 @@ class MediaLibraryTest extends WebDriverTestBase {
$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 @@ class MediaLibraryTest extends WebDriverTestBase {
$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 @@ class MediaLibraryTest extends WebDriverTestBase {
$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 @@ class MediaLibraryTest extends WebDriverTestBase {
$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 @@ class MediaLibraryTest extends WebDriverTestBase {
$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 we can now only upload one more media item.
$this->assertFalse($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
// 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');
$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()));
// Both the type_three and type_four media types accept jpg images.
$assert_session->buttonExists('Type Three');
$assert_session->buttonExists('Type Four')->click();
// Assert that jpg files are accepted by type four.
$page->attachFileToField('Add file', $file_system->realpath($jpg_image->uri));
$assert_session->assertWaitOnAjaxRequest();
// 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);
$page->fillField('Alternative text', $this->randomString());
// The type_four media type has another optional image field.
@ -637,6 +740,14 @@ class MediaLibraryTest extends WebDriverTestBase {
$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);
}

View File

@ -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);
}
}