diff --git a/core/modules/media/src/Element/MediaManagedFile.php b/core/modules/media/src/Element/MediaManagedFile.php index e3fbbe8..af98d19 100644 --- a/core/modules/media/src/Element/MediaManagedFile.php +++ b/core/modules/media/src/Element/MediaManagedFile.php @@ -11,6 +11,24 @@ */ class MediaManagedFile extends ManagedFile { + /** + * {@inheritdoc} + */ + public function getInfo() { + $element = parent::getInfo(); + + $class = get_class($this); + $element['#element_validate'] = [ + [$class, 'validateManagedFile'], + [$class, 'validateEntityForm'], + ]; + + return $element; + } + + /** + * {@inheritdoc} + */ public static function processManagedFile(&$element, FormStateInterface $form_state, &$complete_form) { if (!empty($element['#value']['target_id'])) { // We're editing an entity with a previously-saved Media entity @@ -40,4 +58,19 @@ public static function processManagedFile(&$element, FormStateInterface $form_st return $element; } + /** + * Validates the media form using our custom form handler. + * + * @param array $entity_form + * The entity form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + */ + public static function validateEntityForm(&$entity_form, FormStateInterface $form_state) { + $form_mode = 'add_inline'; + $inline_form_handler = \Drupal::entityTypeManager() + ->getHandler('media', $form_mode); + $inline_form_handler->entityFormValidate($entity_form, $form_state); + } + } diff --git a/core/modules/media/src/Form/MediaInlineForm.php b/core/modules/media/src/Form/MediaInlineForm.php index 988de58..baae472 100644 --- a/core/modules/media/src/Form/MediaInlineForm.php +++ b/core/modules/media/src/Form/MediaInlineForm.php @@ -9,6 +9,7 @@ use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\Element; use Drupal\media\MediaInlineFormInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -79,16 +80,9 @@ public static function createInstance(ContainerInterface $container, EntityTypeI /** * {@inheritdoc} */ - public function getEntityType() { - return $this->entityType; - } - - /** - * {@inheritdoc} - */ public function entityForm(array $entity_form, FormStateInterface $form_state) { // Build the media entity form. - /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ + /** @var \Drupal\media\MediaInterface $entity */ $entity = $entity_form['#entity']; $form_display = $this->getFormDisplay($entity, $entity_form['#form_mode']); $form_display->buildForm($entity, $entity_form, $form_state); @@ -96,17 +90,18 @@ public function entityForm(array $entity_form, FormStateInterface $form_state) { // In the service of keeping this form as user-friendly as possible in the // context of a parent entity form, only show required fields. - /** @var \Drupal\Core\Entity\EntityFieldManager $entity_field_manager */ - $entity_field_manager = \Drupal::service('entity_field.manager'); - $field_definitions = $entity_field_manager->getFieldDefinitions('media', 'file'); + $field_definitions = $this->entityFieldManager->getFieldDefinitions('media', $entity->bundle()); /** @var \Drupal\Core\Field\BaseFieldDefinition $field_definition */ foreach ($field_definitions as $field_definition) { $field_name = $field_definition->getName(); if (!$field_definition->isRequired()) { $entity_form[$field_name]['#access'] = FALSE; } - else { - $entity_form[$field_name]['#element_validate'] = []; + elseif (!empty($entity_form[$field_name])) { + // Elements with "#required" key set will always be validated, even if + // 'limit_validation_errors' is set. Disable their validation here, we + // will enforce validation happens inside the real submit handler. + $entity_form[$field_name] = $this->disableElementChildrenValidation($entity_form[$field_name]); } } @@ -118,7 +113,8 @@ public function entityForm(array $entity_form, FormStateInterface $form_state) { ->getName(); $entity_form[$source_field]['#access'] = FALSE; - // Inline entities inherit the parent language. + // Inline entities inherit the parent language, so hide translation-related + // fields as well. $langcode_key = $this->entityType->getKey('langcode'); if ($langcode_key && isset($entity_form[$langcode_key])) { $entity_form[$langcode_key]['#access'] = FALSE; @@ -137,6 +133,99 @@ public function entityForm(array $entity_form, FormStateInterface $form_state) { } /** + * {@inheritdoc} + */ + public function entityFormValidate(array &$entity_form, FormStateInterface $form_state) { + $triggering_element = $form_state->getTriggeringElement(); + // Perform entity validation only if the inline form was submitted, + // skipping other requests such as file uploads. + if (!$this->hostFormSubmitted($form_state)) { + return; + } + $mids = []; + $form_mids = $form_state->getValue($entity_form['#field_name']); + if (!empty($form_mids)) { + foreach ($form_mids as $form_mid) { + if (!empty($form_mid['mids'][0])) { + $mids[] = $form_mid['mids'][0]; + } + } + } + foreach ($mids as $mid) { + $media_form_key = 'media_' . $mid; + if (empty($entity_form[$media_form_key]['media_form']['form'])) { + continue; + } + $media_form = $entity_form[$media_form_key]['media_form']['form']; + /** @var \Drupal\media\MediaInterface $entity */ + $entity = $media_form['#entity']; + if ($entity && is_a($entity, '\Drupal\Core\Entity\ContentEntityInterface')) { + $this->buildEntity($media_form, $entity, $form_state); + $form_display = $this->getFormDisplay($entity, $media_form['#form_mode']); + $form_display->validateFormValues($entity, $media_form, $form_state); + $entity->setValidationRequired(FALSE); + + foreach($form_state->getErrors() as $name => $message) { + // $name may be unknown in $form_state and + // $form_state->setErrorByName($name, $message) may suppress the error + // message. + $form_state->setError($triggering_element, $message); + } + + // If no validation errors are present, set the media back to Published. + if (empty($form_state->getErrors())) { + $entity->setPublished()->save(); + } + } + } + } + + /** + * Bypass validation in all children of a given element. + * + * @param array $element + * The element array. + * + * @return array + * The same element array, after recursively checking all its children and + * setting "#validated" => TRUE in all required elements. + */ + private function disableElementChildrenValidation(array $element) { + foreach (Element::children($element) as $key) { + if (isset($element[$key]) && $element[$key]) { + $element[$key] = $this->disableElementChildrenValidation($element[$key]); + } + } + + if (!empty($element['#needs_validation']) || !empty($element['#required'])) { + $element['#validated'] = TRUE; + } + + return $element; + } + + /** + * Builds an updated entity object based upon the submitted form values. + * + * @param array $entity_form + * The entity form. + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * The entity. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + */ + protected function buildEntity(array $entity_form, ContentEntityInterface $entity, FormStateInterface $form_state) { + $form_display = $this->getFormDisplay($entity, $entity_form['#form_mode']); + $form_display->extractFormValues($entity, $entity_form, $form_state); + // Invoke all specified builders for copying form values to entity fields. + if (isset($entity_form['#entity_builders'])) { + foreach ($entity_form['#entity_builders'] as $function) { + call_user_func_array($function, [$entity->getEntityTypeId(), $entity, &$entity_form, &$form_state]); + } + } + } + + /** * Gets the form display for the given entity. * * @param \Drupal\Core\Entity\ContentEntityInterface $entity @@ -151,4 +240,23 @@ protected function getFormDisplay(ContentEntityInterface $entity, $form_mode) { return EntityFormDisplay::collectRenderDisplay($entity, $form_mode); } + /** + * Check if the form processing is due to a host submit. + * + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The $form_state object. + * + * @return bool + * TRUE if the form is being processed due to the host entity's form being + * submitted (and not other AJAX-based submissions, such as uploads, etc.). + */ + private function hostFormSubmitted(FormStateInterface $form_state) { + $triggering_element = $form_state->getTriggeringElement(); + // @TODO Figure out a more robust way of doing this. + if (strpos($triggering_element['#name'], '_button') === FALSE) { + return TRUE; + } + return FALSE; + } + } diff --git a/core/modules/media/src/MediaInlineFormInterface.php b/core/modules/media/src/MediaInlineFormInterface.php index fc6ea3a..b65e6d4 100644 --- a/core/modules/media/src/MediaInlineFormInterface.php +++ b/core/modules/media/src/MediaInlineFormInterface.php @@ -6,19 +6,11 @@ use Drupal\Core\Form\FormStateInterface; /** - * Defines the interface for inline form handlers. + * Defines the interface for Media inline form handlers. */ interface MediaInlineFormInterface extends EntityHandlerInterface { /** - * Gets the entity type managed by this handler. - * - * @return \Drupal\Core\Entity\EntityTypeInterface - * The entity type. - */ - public function getEntityType(); - - /** * Builds the entity form. * * @param array $entity_form @@ -33,4 +25,14 @@ public function getEntityType(); */ public function entityForm(array $entity_form, FormStateInterface $form_state); + /** + * Validates the entity form. + * + * @param array $entity_form + * The entity form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state of the parent form. + */ + public function entityFormValidate(array &$entity_form, FormStateInterface $form_state); + } diff --git a/core/modules/media/src/Plugin/Field/FieldWidget/MediaFileWidget.php b/core/modules/media/src/Plugin/Field/FieldWidget/MediaFileWidget.php index 7ac3eb3..6a0a4c5 100644 --- a/core/modules/media/src/Plugin/Field/FieldWidget/MediaFileWidget.php +++ b/core/modules/media/src/Plugin/Field/FieldWidget/MediaFileWidget.php @@ -2,8 +2,12 @@ namespace Drupal\media\Plugin\Field\FieldWidget; +use Drupal\Core\Entity\ContentEntityInterface; +use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\ElementInfoManagerInterface; +use Drupal\Core\Render\RendererInterface; use Drupal\file\Entity\File; use Drupal\file\Plugin\Field\FieldWidget; use Drupal\file\Plugin\Field\FieldWidget\FileWidget; @@ -13,6 +17,7 @@ use Drupal\media\Entity\MediaType; use Drupal\Component\Utility\NestedArray; use Drupal\Core\Link; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Plugin implementation of the 'media_file' widget. @@ -28,6 +33,36 @@ class MediaFileWidget extends FileWidget { /** + * The renderer. + * + * @var \Drupal\Core\Render\RendererInterface + */ + protected $renderer; + + /** + * {@inheritdoc} + */ + public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, ElementInfoManagerInterface $element_info, RendererInterface $renderer) { + parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings, $element_info); + $this->renderer = $renderer; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $plugin_id, + $plugin_definition, + $configuration['field_definition'], + $configuration['settings'], + $configuration['third_party_settings'], + $container->get('element_info'), + $container->get('renderer') + ); + } + + /** * {@inheritdoc} * * Uses nearly the same code that FileWidget does here, except that we have to @@ -53,14 +88,14 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen 'description' => '', ]; - /** @var \Drupal\media\Entity\MediaType $media_type */ $target_bundle = array_shift($this->fieldDefinition->getSetting('handler_settings')['target_bundles']); + /** @var \Drupal\media\Entity\MediaType $media_type */ $media_type = MediaType::load($target_bundle); $source = $media_type->getSource(); $source_data_definition = FieldItemDataDefinition::create($source->getSourceFieldDefinition($media_type)); $file_item = new FileItem($source_data_definition); - $element_info = $this->elementInfo->getInfo('media_managed_file'); + $element_info = $this->elementInfo->getInfo('media_managed_file'); $cardinality = $this->fieldDefinition->getFieldStorageDefinition()->getCardinality(); $element += [ @@ -74,9 +109,9 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen '#extended' => TRUE, // Add properties needed by value() and process() methods. '#field_name' => $this->fieldDefinition->getName(), - // This is actually talking about the bundle ON WHICH this field is - // placed, not the bundle(s) TO WHICH it can refer. In fact, it seems only - // to be used, unnecessarily, on FileWidget::validateMultipleCount(). + // This is actually talking about the entity type ON WHICH this field is + // placed, not the entity type TO WHICH it can refer. In fact, it seems + // only to be used, unnecessarily, on FileWidget::validateMultipleCount(). '#entity_type' => $items->getEntity()->getEntityTypeId(), '#display_field' => (bool) $field_settings['display_field'], '#display_default' => $field_settings['display_default'], @@ -100,7 +135,7 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen '#upload_validators' => $element['#upload_validators'], '#cardinality' => $cardinality, ]; - $element['#description'] = \Drupal::service('renderer')->renderPlain($file_upload_help); + $element['#description'] = $this->renderer->renderPlain($file_upload_help); $element['#multiple'] = $cardinality != 1 ? TRUE : FALSE; if ($cardinality != 1 && $cardinality != -1) { $element['#element_validate'][] = [get_class($this), 'validateMultipleCount']; @@ -118,6 +153,7 @@ public function massageFormValues(array $values, array $form, FormStateInterface // entities instead of files. $new_values = []; foreach ($values as &$value) { + $value['mids'] = !empty($value['mids']) ? $value['mids'] : []; foreach ($value['mids'] as $mid) { $new_value = $value; $new_value['target_id'] = $mid; @@ -130,28 +166,42 @@ public function massageFormValues(array $values, array $form, FormStateInterface } /** - * {@inheritdoc} + * Retrieves the value for the element. */ public static function value($element, $input, FormStateInterface $form_state) { $return = parent::value($element, $input, $form_state); + $media_type = FALSE; + $host_entity = $form_state->getFormObject()->getEntity(); + if ($host_entity && $host_entity instanceof ContentEntityInterface) { + $handler_settings = $host_entity->getFieldDefinitions()[$element['#field_name']]->getSetting('handler_settings'); + // If this field happens to point to multiple media types (bundles), just + // use the first one that implements a file source. + $media_type = !empty($handler_settings['target_bundles']) ? self::getFileMediaTypeFromSet($handler_settings['target_bundles']) : FALSE; + } + // When the form has just been submitted with file(s) being uploaded, // $input['mids'] should be empty. FileWidget::value() calls // ManagedFile::valueCallback(), which populates $return['fids']. If there's // info in there AND we're not doing the subsequent form rebuild, it's time // to make the media. - if (empty($input['mids']) && !empty($return['fids']) && !$form_state->isRebuilding()) { + if (empty($input['mids']) && !empty($return['fids']) && !$form_state->isRebuilding() && $media_type) { foreach ($return['fids'] as $fid) { $file = File::load($fid); /** @var \Drupal\media\MediaInterface $media_entity */ $media_entity = Media::create([ + // @TODO: Shouldn't media entities automatically deal with empty names? 'name' => $file->getFilename(), - 'bundle' => 'file', // TODO: generalize (note that we have to apply this to MediaInlineFileWidgetTest::setUp() too, in createdMediaType() call) + 'bundle' => $media_type, 'uid' => \Drupal::currentUser()->id(), // TODO: figure out langcode in this context. // 'langcode' => !empty($element['#langcode']) ? $element['#langcode'] : LanguageInterface::LANGCODE_DEFAULT, ]); - $media_entity->setPublished(); + // We create media items as unpublished to prevent "abandoned" uploads + // from producing visible media items on the site. In + // \Drupal\media\Form\MediaInlineForm::entityFormValidate() we make sure + // we set them back to published when the main form is submitted. + $media_entity->setUnpublished(); $source_field = $media_entity->getSource() ->getSourceFieldDefinition($media_entity->bundle->entity) ->getName(); @@ -160,13 +210,13 @@ public static function value($element, $input, FormStateInterface $form_state) { $return['mids'][] = $media_entity->id(); } } - else if (!empty($input['mids'])) { + elseif (!empty($input['mids'])) { $mid = $input['mids']; - $media_fields = 'media_' . $mid; + $media_fields_key = 'media_' . $mid; - if (!empty($input[$media_fields])) { + if (!empty($input[$media_fields_key])) { $media_entity = Media::load($mid); - foreach ($input[$media_fields] as $field_name => $field_value) { + foreach ($input[$media_fields_key] as $field_name => $field_value) { $media_entity->set($field_name, $field_value); } $media_entity->save(); @@ -175,14 +225,14 @@ public static function value($element, $input, FormStateInterface $form_state) { // Warn process() that this is a preexisting value. $return['media_already_saved'] = TRUE; } - $return['mids'] = [$input['mids']]; + $return['mids'] = [$mid]; } return $return; } /** - * {@inheritdoc} + * Expands the file element to include the inline version of the media form. */ public static function process($element, FormStateInterface $form_state, $form) { $element = parent::process($element, $form_state, $form); @@ -194,7 +244,7 @@ public static function process($element, FormStateInterface $form_state, $form) // first time. Just show the icon and name. $mid = $element['#value']['target_id']; } - else if (!empty($element['#value']['mids']) && count($element['#value']['mids']) === 1) { + elseif (!empty($element['#value']['mids']) && count($element['#value']['mids']) === 1) { // If $element['mids']['#value'] has multiple values, that means this is // the first pass after someone uploaded several files; ignore. If it has // only ONE, then this media entity was just created and we want to force @@ -228,8 +278,8 @@ public static function process($element, FormStateInterface $form_state, $form) '#uri' => $media_entity->getSource()->getMetadata($media_entity, 'thumbnail_uri'), '#weight' => -20, ]; - $link_ra = Link::createFromRoute($media_entity->getName(), 'entity.media.canonical', ['media' => $mid])->toRenderable(); - $element[$form_element_name]['media_name'] = $link_ra + ['weight' => -10]; + $link_build = Link::createFromRoute($media_entity->getName(), 'entity.media.canonical', ['media' => $mid])->toRenderable(); + $element[$form_element_name]['media_name'] = $link_build + ['#weight' => -10]; if ($show_form) { // Create and add the media entity form. @@ -242,8 +292,10 @@ public static function process($element, FormStateInterface $form_state, $form) $inline_form_handler = \Drupal::entityTypeManager() ->getHandler('media', $form_mode); $media_form = $inline_form_handler->entityForm($media_form, $form_state); - - $element[$form_element_name] += $media_form; + $element[$form_element_name]['media_form'] = [ + '#type' => 'container', + 'form' => $media_form, + ]; } } @@ -307,4 +359,60 @@ public static function submit($form, FormStateInterface $form_state) { static::setWidgetState($parents, $field_name, $form_state, $field_state); } + /** + * {@inheritdoc} + */ + public static function isApplicable(FieldDefinitionInterface $field_definition) { + // This widget is restricted to entity_reference fields pointing to media + // entities. + if ($field_definition->getSetting('target_type') !== 'media') { + return FALSE; + } + + // This widget can be chosen only if the field points to a single target + // Media Type, and this type has a source of type "File". + $handler_settings = $field_definition->getSetting('handler_settings'); + if (empty($handler_settings['target_bundles']) || count($handler_settings['target_bundles']) !== 1) { + return FALSE; + } + $type_name = reset($handler_settings['target_bundles']); + $media_type = MediaType::load($type_name); + if ($media_type) { + $source = $media_type->getSource(); + if (is_a($source, '\Drupal\media\Plugin\media\Source\File')) { + return TRUE; + } + } + + return FALSE; + } + + /** + * Given a set of media types, fetch the first one with a File source. + * + * @param array $types + * An array where the values are media type IDs. + * + * @return bool|string + * The ID of the first type found (from the set passed in) with a source + * plugin that implements + * \Drupal\media\Plugin\media\Source\File, or FALSE if none found. + */ + public static function getFileMediaTypeFromSet(array $types) { + $media_types = MediaType::loadMultiple($types); + if (!$media_types) { + return FALSE; + } + + foreach ($media_types as $media_type) { + /** @var \Drupal\media\MediaTypeInterface $media_type */ + $source = $media_type->getSource(); + if (is_a($source, '\Drupal\media\Plugin\media\Source\File')) { + return $media_type->id(); + } + } + + return FALSE; + } + } diff --git a/core/modules/media/tests/src/FunctionalJavascript/MediaInlineFileWidgetTest.php b/core/modules/media/tests/src/FunctionalJavascript/MediaInlineFileWidgetTest.php index f35addb..b109fc7 100644 --- a/core/modules/media/tests/src/FunctionalJavascript/MediaInlineFileWidgetTest.php +++ b/core/modules/media/tests/src/FunctionalJavascript/MediaInlineFileWidgetTest.php @@ -4,7 +4,6 @@ use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; -use Drupal\Tests\field\Unit\FieldStorageConfigAccessControlHandlerTest; /** * @group media @@ -18,7 +17,26 @@ class MediaInlineFileWidgetTest extends MediaJavascriptTestBase { protected $strictConfigSchema = FALSE; - protected $node_type; + /** + * The bundle name of the test node. + * + * @var string + */ + protected $nodeTypeId; + + /** + * The bundle name of the test media. + * + * @var string + */ + protected $mediaTypeId; + + /** + * An indexed array of valid paths for test files. + * + * @var array + */ + protected $testFilePaths; /** * {@inheritdoc} @@ -26,9 +44,10 @@ class MediaInlineFileWidgetTest extends MediaJavascriptTestBase { protected function setUp() { parent::setUp(); $media_type = $this->createMediaType(['bundle' => 'file'], 'file'); + $this->mediaTypeId = $media_type->id(); + // Create two test fields on this media type, one of them required. $media_required_field_storage = FieldStorageConfig::create([ 'type' => 'string', - 'cardinality' => -1, 'entity_type' => 'media', 'field_name' => 'field_media_required', 'settings' => [ @@ -36,20 +55,56 @@ protected function setUp() { ], ]); $media_required_field_storage->save(); - $media_required_field = FieldConfig::create([ + FieldConfig::create([ 'field_storage' => $media_required_field_storage, - 'bundle' => $media_type->id(), + 'bundle' => $this->mediaTypeId, 'required' => TRUE, + ])->save(); + entity_get_form_display('media', $media_type->id(), 'add_inline') + ->setComponent('field_media_required', ['type' => 'string_textfield']) + ->save(); + $media_nonrequired_field_storage = FieldStorageConfig::create([ + 'type' => 'string', + 'entity_type' => 'media', + 'field_name' => 'field_media_nonrequired', + 'settings' => [ + 'max_length' => 255, + ], ]); - $media_required_field->save(); -// $media_form_display = entity_get_form_display('media', $media_type->id(), 'add_inline'); -// $media_form_display->setComponent('field_media_required', ['type' => 'string_textfield']); -// $media_form_display->save(); + $media_nonrequired_field_storage->save(); + FieldConfig::create([ + 'field_storage' => $media_nonrequired_field_storage, + 'bundle' => $this->mediaTypeId, + 'required' => FALSE, + ])->save(); + // Deliberately add it to the form, to test that even then we don't show + // this field when embedding the form inside the widget. + entity_get_form_display('media', $this->mediaTypeId, 'add_inline') + ->setComponent('field_media_nonrequired', ['type' => 'string_textfield']) + ->save(); + entity_get_display('media', $this->mediaTypeId, 'default') + ->setComponent('field_media_file', [ + 'type' => 'file_default', + 'label' => 'hidden', + 'settings' => [], + ]) + ->setComponent('field_media_required', [ + 'type' => 'string', + 'label' => 'hidden', + 'settings' => [], + ]) + ->setComponent('field_media_nonrequired', [ + 'type' => 'string', + 'label' => 'hidden', + 'settings' => [], + ]) + ->save(); $node_type = $this->drupalCreateContentType(); - $this->node_type = $node_type->id(); + $this->nodeTypeId = $node_type->id(); $field_storage = FieldStorageConfig::create([ 'type' => 'entity_reference', + 'cardinality' => -1, 'entity_type' => 'node', 'field_name' => 'field_media_reference', 'settings' => [ @@ -57,43 +112,165 @@ protected function setUp() { ], ]); $field_storage->save(); - $field = FieldConfig::create([ + FieldConfig::create([ 'field_storage' => $field_storage, - 'bundle' => $node_type->id(), + 'bundle' => $this->nodeTypeId, 'settings' => [ - 'file_extensions' => 'pdf', 'handler_settings' => [ 'target_bundles' => [ - $media_type->id(), + $this->mediaTypeId => $this->mediaTypeId, ], ], ], - ]); - $field->save(); - $form_display = entity_get_form_display('node', $node_type->id(), 'default'); - $form_display->setComponent('field_media_reference', ['type' => 'media_file']); - $form_display->save(); + ])->save(); + entity_get_form_display('node', $this->nodeTypeId, 'default') + ->setComponent('field_media_reference', ['type' => 'media_file']) + ->save(); + entity_get_display('node', $this->nodeTypeId, 'default') + ->setComponent('field_media_reference', [ + 'type' => 'entity_reference_entity_view', + 'label' => 'hidden', + 'settings' => [ + 'view_mode' => 'full', + ], + ])->save(); + + // Create a couple of files for testing. + for ($i = 0; $i < 2; $i++) { + $test_filename = $this->randomMachineName() . '.txt'; + $test_filepath = 'public://' . $test_filename; + file_put_contents($test_filepath, $this->randomMachineName()); + $this->testFilePaths[$i] = \Drupal::service('file_system')->realpath($test_filepath); + } + } /** - * {@inheritdoc} + * Tests the file widget behavior. */ public function testInlineFileWidget() { - $this->drupalGet('/node/add/' . $this->node_type); - $assert = $this->assertSession(); - $element = $assert->elementExists('css', 'input[type="file"]'); - $filepath = \Drupal::root() . '/' . drupal_get_path('module', 'media') . '/tests/fixtures/example_1.pdf'; - $this->getSession()->getPage()->attachFileToField($element->getAttribute('id'), $filepath); - // Does the new media item's "name" field exist? - $this->waitUntilVisible('input[value="example_1.pdf"]', 10000); - // Does the other required field for this media type exist? - $assert->elementExists('css', 'input[name*="field_media_required"]'); // fails - // Can I upload a second file and get a second media item's "name" field? - $element = $assert->elementExists('css', 'input[type="file"]'); // fails - $filepath = \Drupal::root() . '/' . drupal_get_path('module', 'media') . '/tests/fixtures/example_2.pdf'; - $this->getSession()->getPage()->attachFileToField($element->getAttribute('id'), $filepath); - $this->waitUntilVisible('input[value="example_2.pdf"]', 10000); - // Next: save the node and then follow a link to a working media entity. + $assert_session = $this->assertSession(); + $page = $this->getSession()->getPage(); + + $this->drupalGet('/node/add/' . $this->nodeTypeId); + + // Ensure the media field is present, but no required fields exist in there. + $media_field = $assert_session->elementExists('css', 'details[data-drupal-selector="edit-field-media-reference"]'); + $assert_session->elementNotExists('css', '.required', $media_field); + + // Upload a file and see that new required fields appeared. + $first_upload_element_id = 'edit-field-media-reference-0-upload'; + $page->attachFileToField($first_upload_element_id, $this->testFilePaths[0]); + $result = $assert_session->waitForButton('Remove'); + $this->assertNotEmpty($result); + $media_field = $assert_session->elementExists('css', 'details[data-drupal-selector="edit-field-media-reference"]'); + // The name field should be present and required. + $assert_session->elementExists('css', '.field--name-name input.required', $media_field); + // The additional field sould be present and required. + $assert_session->elementExists('css', '.field--name-field-media-required input.required', $media_field); + // The non-required field should not be there. + $assert_session->elementNotExists('css', '.field--name-field-media-nonrequired input', $media_field); + + // By now the media entity should already exist, but unpublished. + $filename1 = basename($this->testFilePaths[0]); + $media = $this->container->get('entity_type.manager')->getStorage('media') + ->loadByProperties(['name' => $filename1]); + $media = reset($media); + /** @var \Drupal\media\MediaInterface $media */ + $this->assertNotNull($media); + $this->assertFalse($media->isPublished()); + + // Save the node and check page elements correspond to what is expected. + $node_title = 'Host Node 1'; + $page->fillField('Title', $node_title); + $required_text = $this->randomMachineName(); + $page->fillField('field_media_required', $required_text); + $page->pressButton('Save'); + // The node has been correctly saved. + $assert_session->pageTextContains("{$this->nodeTypeId} $node_title has been created"); + // We have a reference media container. + $media_field = $assert_session->elementExists('css', '.field--name-field-media-reference'); + // We have a file link inside that container. + $assert_session->elementExists('css', '.field--name-field-media-file a', $media_field); + // The required field is present. + $required_element = $assert_session->elementExists('css', '.field--name-field-media-required', $media_field); + // The required field text is what we expect. + $this->assertEquals($required_text, $required_element->getText()); + + // The media item should now be published. + $media = $this->container->get('entity_type.manager')->getStorage('media') + ->loadUnchanged($media->id()); + /** @var \Drupal\media\MediaInterface $media */ + $this->assertTrue($media->isPublished()); + + // Edit the node, add a second file, check the widget supports multiple + // values and creates a second media without messing up. + $node = $this->container->get('entity_type.manager')->getStorage('node') + ->loadByProperties(['title' => $node_title]); + $node = reset($node); + $this->drupalGet("/node/{$node->id()}/edit"); + // First file is still there, and remove button is present. + $assert_session->elementContains('css', '#edit-field-media-reference-table', $filename1); + $media_field = $assert_session->elementExists('css', 'details[data-drupal-selector="edit-field-media-reference"]'); + $remove_button = $media_field->findButton('Remove'); + $this->assertNotNull($remove_button); + $second_upload_element_id = 'edit-field-media-reference-1-upload'; + // Upload the second file. + $filename2 = basename($this->testFilePaths[1]); + $page->attachFileToField($second_upload_element_id, $this->testFilePaths[1]); + $assert_session->assertWaitOnAjaxRequest(); + + // Fill in the second media required field and save. + $required_text2 = $this->randomMachineName(); + $page->fillField('field_media_required', $required_text2); + $page->pressButton('Save'); + + // Check everything was saved correctly. + $assert_session->pageTextContains("{$this->nodeTypeId} $node_title has been updated"); + $assert_session->pageTextContains($filename1); + $assert_session->pageTextContains($filename2); + $assert_session->pageTextContains($required_text); + $assert_session->pageTextContains($required_text2); + + // Both media entities should exist now. + foreach ([$filename1, $filename2] as $filename) { + $media = $this->container->get('entity_type.manager')->getStorage('media') + ->loadByProperties(['name' => $filename]); + $media = reset($media); + /** @var \Drupal\media\MediaInterface $media */ + $this->assertTrue($media->isPublished()); + } + + // Edit the node, verify we can successfuly remove one of the items. + $this->drupalGet("/node/{$node->id()}/edit"); + $assert_session->pageTextContains($filename1); + $assert_session->pageTextContains($filename2); + $assert_session->buttonExists('edit-field-media-reference-0-remove-button'); + $page->pressButton('edit-field-media-reference-0-remove-button'); + $assert_session->assertWaitOnAjaxRequest(); + $assert_session->pageTextNotContains($filename1); + $assert_session->pageTextContains($filename2); + $page->pressButton('Save'); + $assert_session->pageTextContains("{$this->nodeTypeId} $node_title has been updated"); + $assert_session->pageTextNotContains($filename1); + $assert_session->pageTextContains($filename2); + $assert_session->pageTextNotContains($required_text); + $assert_session->pageTextContains($required_text2); + + // Removing the file without saving the node should also be possible. + $this->drupalGet('/node/add/' . $this->nodeTypeId); + $first_upload_element_id = 'edit-field-media-reference-0-upload'; + $page->attachFileToField($first_upload_element_id, $this->testFilePaths[0]); + $result = $assert_session->waitForButton('Remove'); + $this->assertNotEmpty($result); + $media_field = $assert_session->elementExists('css', 'details[data-drupal-selector="edit-field-media-reference"]'); + // The additional field sould be present and required. + $assert_session->elementExists('css', '.field--name-field-media-required input.required', $media_field); + $page->pressButton('Remove'); + $assert_session->assertWaitOnAjaxRequest(); + $media_field = $assert_session->elementExists('css', 'details[data-drupal-selector="edit-field-media-reference"]'); + // The non-required field should have disappeared. + $assert_session->elementNotExists('css', '.field--name-field-media-required input.required', $media_field); } }