diff --git a/core/modules/media/config/install/core.entity_form_mode.media.add_inline.yml b/core/modules/media/config/install/core.entity_form_mode.media.add_inline.yml new file mode 100644 index 0000000000..e082019a82 --- /dev/null +++ b/core/modules/media/config/install/core.entity_form_mode.media.add_inline.yml @@ -0,0 +1,9 @@ +langcode: en +status: true +dependencies: + module: + - media +id: media.add_inline +label: 'File Upload' +targetEntityType: media +cache: true diff --git a/core/modules/media/src/Entity/Media.php b/core/modules/media/src/Entity/Media.php index 406b45127b..767abfad83 100644 --- a/core/modules/media/src/Entity/Media.php +++ b/core/modules/media/src/Entity/Media.php @@ -39,6 +39,7 @@ * "edit" = "Drupal\media\MediaForm", * "delete" = "Drupal\Core\Entity\ContentEntityDeleteForm", * }, + * "add_inline" = "Drupal\media\Form\MediaInlineForm", * "translation" = "Drupal\content_translation\ContentTranslationHandler", * "views_data" = "Drupal\media\MediaViewsData", * "route_provider" = { diff --git a/core/modules/media/src/Form/MediaInlineForm.php b/core/modules/media/src/Form/MediaInlineForm.php new file mode 100644 index 0000000000..3d6ddb5c4b --- /dev/null +++ b/core/modules/media/src/Form/MediaInlineForm.php @@ -0,0 +1,233 @@ +entityType = $entity_type; + } + + /** + * {@inheritdoc} + */ + public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { + return new static($entity_type); + } + + /** + * Builds the entity form. + * + * @param array $entity_form + * The entity form, containing the following basic properties: + * - #entity: The entity for the current entity form. + * - #form_mode: The form mode used to display the entity form. + * - #parents: Identifies the position of the entity form in the overall + * parent form, and identifies the location where the field values are + * placed within $form_state->getValues(). + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state of the parent form. + * + * @return array + * The entity form array. + */ + public function entityForm(array $entity_form, FormStateInterface $form_state) { + // Build the media entity form. + /** @var \Drupal\media\MediaInterface $entity */ + $entity = $entity_form['#entity']; + $form_display = EntityFormDisplay::collectRenderDisplay($entity, $entity_form['#form_mode']); + $form_display->buildForm($entity, $entity_form, $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\Field\BaseFieldDefinition $field_definition */ + foreach ($entity->getFieldDefinitions() as $field_definition) { + $field_name = $field_definition->getName(); + if (isset($entity_form[$field_name])) { + $entity_form[$field_name]['#access'] = $field_definition->isRequired(); + + if ($field_definition->isRequired()) { + // 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]); + } + } + } + // The media name will be automatically populated by the source plugin. We + // are OK with the default name in this case. + if (isset($entity_form['name'])) { + $entity_form['name']['#access'] = FALSE; + } + + // By this point it is expected that the source field has already been + // populated, so hide it too. An example for the file widget can be found in + // @see \Drupal\media\Plugin\Field\FieldWidget\MediaFileWidget::value(). + $source_field = $entity->getSource() + ->getSourceFieldDefinition($entity->bundle->entity) + ->getName(); + $entity_form[$source_field]['#access'] = FALSE; + + // If this is an image media entity, and the image field on that entity has + // either "Alt field required" or "Title field required", we take care of + // that in MediaImageWidget::process(). + // @see \Drupal\media\Plugin\Field\FieldWidget\MediaImageWidget::process(). + foreach (['#alt_field_required', '#title_field_required'] as $extra_image_field_attribute) { + if (isset($entity_form[$source_field]['widget'][0][$extra_image_field_attribute])) { + $entity_form[$source_field]['widget'][0][$extra_image_field_attribute] = FALSE; + } + } + + // Inline entities inherit the parent language, so hide translation-related + // fields as well. + if (isset($entity_form['langcode'])) { + $entity_form['langcode']['#access'] = FALSE; + } + + return $entity_form; + } + + /** + * 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) { + // Perform entity validation only if the inline form was submitted, + // skipping other requests such as file uploads. + if (!$this->hostFormSubmitted($form_state)) { + return; + } + $mids = []; + // @todo use #parents if possible. + $field_values = $form_state->getValue($entity_form['#field_name']); + if ($field_values) { + foreach ($field_values as $field_value) { + if (!empty($field_value['mids'])) { + $mids[] = reset($field_value['mids']); + } + } + } + foreach ($mids as $mid) { + $media_form_key = 'media_' . $mid; + // @todo throw error in this case instead of quietly continuing. + if (empty($entity_form[$media_form_key]['form_wrapper']['form'])) { + continue; + } + $media_form = $entity_form[$media_form_key]['form_wrapper']['form']; + /** @var \Drupal\media\MediaInterface $entity */ + $entity = $media_form['#entity']; + $triggering_element = $form_state->getTriggeringElement(); + $this->buildEntity($media_form, $entity, $form_state); + $form_display = EntityFormDisplay::collectRenderDisplay($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 = EntityFormDisplay::collectRenderDisplay($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]); + } + } + } + + /** + * 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.). + */ + protected 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/Plugin/Field/FieldWidget/MediaFileWidget.php b/core/modules/media/src/Plugin/Field/FieldWidget/MediaFileWidget.php new file mode 100644 index 0000000000..7b16fe1447 --- /dev/null +++ b/core/modules/media/src/Plugin/Field/FieldWidget/MediaFileWidget.php @@ -0,0 +1,422 @@ +get('element_info') + ); + } + + /** + * Gets the default value structure this widget expects. + * + * @return array + */ + protected static function getDefaultValues() { + return [ + 'mids' => [], + 'fids' => [], + ]; + } + + /** + * {@inheritdoc} + * + * Uses nearly the same code that FileWidget does here, except that we have to + * dig down through the target Media entity to *its* file field's upload + * location and upload validators. + */ + public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) { + // We've ensured that there's only one target bundle. Get an instance of + // that bundle in order to get its source field. + // @see \Drupal\media\Plugin\Field\FieldWidget\MediaFileWidget::isApplicable() + $target_bundle = reset($this->fieldDefinition->getSetting('handler_settings')['target_bundles']); + /** @var \Drupal\media\Entity\MediaType $media_type */ + $media_type = MediaType::load($target_bundle); + /** @var \Drupal\media\MediaInterface $media_item */ + $media_item = Media::create(['bundle' => $target_bundle]); + $source_field_definition = $media_item->getSource()->getSourceFieldDefinition($media_type); + $source_field_items = $media_item->get($source_field_definition->getName()); + $source_field_items->appendItem(); + + // Temporarily use the source field's fieldDefinition, so that + // FileWidget::formElement() returns a usable $element array. + // @see \Drupal\file\Plugin\Field\FieldWidget\FileWidget\FileWidget::formElement() + $real_field_definition = $this->fieldDefinition; + $this->fieldDefinition = $source_field_definition; + $element = parent::formElement($source_field_items, 0, $element, $form, $form_state); + $this->fieldDefinition = $real_field_definition; + + // Now modify $element to account for the fact that it was originally built + // to reference file entities, whereas we are going to reference media + // entities. + $element['#element_validate'] = [ + [static::class, 'validateEntityForm'], + ]; + $element['#progress_indicator'] = $this->getSetting('progress_indicator'); + $element['#field_name'] = $this->fieldDefinition->getName(); + $element['#weight'] = $delta; + + // Accommodate multiple uploads, if field settings require it. + $cardinality = $this->fieldDefinition->getFieldStorageDefinition()->getCardinality(); + $element['#cardinality'] = $cardinality; + $element['#multiple'] = $cardinality != 1 ? TRUE : FALSE; + if ($cardinality != 1 && $cardinality != -1) { + $element['#element_validate'][] = [static::class, 'validateMultipleCount']; + } + + // Save mid, the target_id value that this field ultimately cares about. + if (!isset($items[$delta]->mids) && isset($items[$delta]->target_id)) { + $items[$delta]->mids = [$items[$delta]->target_id]; + } + + // Finally, fill in any needed defaults that haven't already been set. + $element['#default_value'] = $items[$delta]->getValue() + static::getDefaultValues(); + + return $element; + } + + /** + * {@inheritdoc} + */ + public function massageFormValues(array $values, array $form, FormStateInterface $form_state) { + // Override FileWidget::massageFormValues() in order to reference Media + // 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; + unset($new_value['mids']); + $new_values[] = $new_value; + } + } + + return $new_values; + } + + /** + * {@inheritdoc} + */ + public static function value($element, $input, FormStateInterface $form_state) { + $return = parent::value($element, $input, $form_state); + $return['mids'] = !empty($return['mids']) ? $return['mids'] : []; + + $form_object = $form_state->getFormObject(); + if (!$form_object instanceOf EntityFormInterface) { + throw new \LogicException('We are in bat country.'); + } + + // Although this method is mainly used as a value callback for real user- + // entered values, it is also called when site builders are configuring the + // field. In that case, $host_entity won't be a FieldableEntityInterface; + // just return the default value structure. + $host_entity = $form_object->getEntity(); + if (!$host_entity instanceof FieldableEntityInterface) { + return static::getDefaultValues(); + } + // We're only allowing this widget to be used when there's a single target + // bundle. That means there's only one item in + // $handler_settings['target_bundles']. + // @see \Drupal\media\Plugin\Field\FieldWidget\MediaFileWidget::isApplicable() + $handler_settings = $host_entity->getFieldDefinitions()[$element['#field_name']]->getSetting('handler_settings'); + $media_type = reset($handler_settings['target_bundles']); + + // This widget target media references. Our element (managed_file) needs + // file ids, so load the file id for it to use. + $get_fid = isset($return['target_id']) && empty($return['fids']); + + // The user has just uploaded file(s); create media accordingly. + $create_media = $input && empty($element['#default_value']['mids']) && !empty($return['fids']); + + // The host form is now being saved with newly-created media entities. They + // might have required field values to save, so save those. + $resave_media = !empty($input['mids']); + + if ($get_fid) { + $media_entity = Media::load($element['#default_value']['target_id']); + $return['fids'][] = $media_entity->getSource()->getSourceFieldValue($media_entity); + } + + if ($create_media) { + foreach ($return['fids'] as $fid) { + /** @var \Drupal\media\MediaInterface $media_entity */ + $media_entity = Media::create([ + 'bundle' => $media_type, + 'uid' => \Drupal::currentUser()->id(), + 'langcode' => $host_entity->language()->getId(), + ]); + // 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(); + $media_entity->set($source_field, $fid); + $media_entity->save(); + $return['mids'][] = $media_entity->id(); + } + } + + if ($resave_media) { + $mid = $input['mids']; + $media_fields_key = 'media_' . $mid; + + if (!empty($input[$media_fields_key])) { + $media_entity = Media::load($mid); + foreach ($input[$media_fields_key] as $field_name => $field_value) { + $media_entity->set($field_name, $field_value); + } + $media_entity->save(); + } + else { + // Warn process() that this is a preexisting value. + $return['media_already_saved'] = TRUE; + } + $return['mids'] = [$mid]; + } + + return $return; + } + + /** + * 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); + + // For readability, working with $value rather than $element['#value']. For + // how $element['#value'] gets set, see massageFormValues() in our related + // classes. + // * @see \Drupal\media\Plugin\Field\FieldWidget\MediaFileWidget::massageFormValues() + // * @see \Drupal\file\Plugin\Field\FieldWidget\FileWidget::massageFormValues() + $value = isset($element['#value']) ? $element['#value'] : []; + + // $value['mids'] would have been set in MediaFileWidget::value() in the + // cases that a) the user has just uploaded a file or files, or b) they are + // viewing a form where a media item had been previously uploaded. + // @see \Drupal\media\Plugin\Field\FieldWidget\MediaFileWidget::value() + $mids = isset($value['mids']) ? $value['mids'] : []; + $element['mids'] = [ + '#type' => 'hidden', + '#value' => $mids, + ]; + + $mid = FALSE; + $show_form = FALSE; + + if (!empty($value['target_id'])) { + // When a media entity reference value was previously saved, then loaded + // again on this host entity form, it has this 'target_id' key. So we know + // this is an existing entity ref; just show the icon and name. + $mid = $value['target_id']; + } + elseif (!empty($value['mids']) && count($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 + // the user to fill out all the new entity's required fields. I.e., show + // the form, as well as the standard icon and name. + $mid = reset($value['mids']); + + if (empty($value['media_already_saved'])) { + $show_form = TRUE; + } + } + + // $mid can be false here, in the event that this value is the final one + // for the media_managed_file element. That value becomes the upload button. + if ($mid) { + // First, get rid of the file link that FileManaged has created. + $fid = reset($value['fids']); + unset($element['file_' . $fid]); + + // Under some circumstances this button said "remove selected," but since + // we just removed the checkbox this button should instead always just say + // "remove." + $element['remove_button']['#value'] = t('Remove'); + // Also hide the upload button. + $element['upload']['#access'] = FALSE; + + // Now show the media icon and name. + $form_element_name = 'media_' . $mid; + $form_element_parents = array_merge($element['#parents'], [$form_element_name]); + $media_entity = Media::load($mid); + + // @todo: entity query to be sure whether or not thumbnail image style + // actually exists. If not stick the user with the "original" image style. + $element[$form_element_name]['icon'] = [ + '#theme' => 'image_style', + '#style_name' => 'thumbnail', + '#uri' => $media_entity->getSource()->getMetadata($media_entity, 'thumbnail_uri'), + '#weight' => -20, + ]; + $link_build = Link::createFromRoute($media_entity->getName(), 'entity.media.canonical', ['media' => $mid])->toRenderable(); + $element[$form_element_name]['link'] = $link_build + ['#weight' => -10]; + + if ($show_form) { + // Create and add the media entity form. + $form_mode = 'add_inline'; + $media_form = [ + '#entity' => $media_entity, + '#form_mode' => $form_mode, + '#parents' => $form_element_parents, + ]; + $inline_form_handler = \Drupal::entityTypeManager() + ->getHandler('media', $form_mode); + $media_form = $inline_form_handler->entityForm($media_form, $form_state); + $element[$form_element_name]['form_wrapper'] = [ + '#type' => 'container', + 'form' => $media_form, + ]; + } + } + + 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(array &$entity_form, FormStateInterface $form_state) { + \Drupal::entityTypeManager()->getHandler('media', 'add_inline') + ->entityFormValidate($entity_form, $form_state); + } + + /** + * Form submission handler for upload/remove button of formElement(). + * + * Overrides FileWidget::submit(), performing the exact same functionality, + * only maintaining awareness of mids as well as fids. + * + * @see \Drupal\file\Plugin\Field\FieldWidget\FileWidget::submit() + * @see file_managed_file_submit() + */ + public static function submit($form, FormStateInterface $form_state) { + // During the form rebuild, formElement() will create field item widget + // elements using re-indexed deltas, so clear out FormState::$input to + // avoid a mismatch between old and new deltas. The rebuilt elements will + // have #default_value set appropriately for the current state of the field, + // so nothing is lost in doing this. + $button = $form_state->getTriggeringElement(); + $parents = array_slice($button['#parents'], 0, -2); + NestedArray::setValue($form_state->getUserInput(), $parents, NULL); + + // Go one level up in the form, to the widgets container. + $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -1)); + $field_name = $element['#field_name']; + $parents = $element['#field_parents']; + + $submitted_values = NestedArray::getValue($form_state->getValues(), array_slice($button['#parents'], 0, -2)); + foreach ($submitted_values as $delta => $submitted_value) { + if (empty($submitted_value['fids'])) { + unset($submitted_values[$delta]); + } + } + + // If there are more files uploaded via the same widget, we have to separate + // them, as we display each file in its own widget. + $new_values = []; + foreach ($submitted_values as $submitted_value) { + if (is_array($submitted_value['fids']) && is_array($submitted_value['mids'])) { + foreach ($submitted_value['fids'] as $delta => $fid) { + $new_value = $submitted_value; + $new_value['fids'] = [$fid]; + $new_value['mids'] = [$submitted_value['mids'][$delta]]; + $new_values[] = $new_value; + } + } + } + + // Re-index deltas after removing empty items. + $submitted_values = array_values($new_values); + + // Update form_state values. + NestedArray::setValue($form_state->getValues(), array_slice($button['#parents'], 0, -2), $submitted_values); + + // Update items. + $field_state = static::getWidgetState($parents, $field_name, $form_state); + $field_state['items'] = $submitted_values; + static::setWidgetState($parents, $field_name, $form_state, $field_state); + } + + /** + * {@inheritdoc} + */ + public static final 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 ($source instanceof File) { + return TRUE; + } + } + + return FALSE; + } + +} diff --git a/core/modules/media/tests/fixtures/example_1.pdf b/core/modules/media/tests/fixtures/example_1.pdf new file mode 100644 index 0000000000..e69de29bb2 diff --git a/core/modules/media/tests/fixtures/example_2.pdf b/core/modules/media/tests/fixtures/example_2.pdf new file mode 100644 index 0000000000..e69de29bb2 diff --git a/core/modules/media/tests/src/FunctionalJavascript/MediaInlineFileWidgetTest.php b/core/modules/media/tests/src/FunctionalJavascript/MediaInlineFileWidgetTest.php new file mode 100644 index 0000000000..d3707281f6 --- /dev/null +++ b/core/modules/media/tests/src/FunctionalJavascript/MediaInlineFileWidgetTest.php @@ -0,0 +1,276 @@ +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', + 'entity_type' => 'media', + 'field_name' => 'field_media_required', + 'settings' => [ + 'max_length' => 255, + ], + ]); + $media_required_field_storage->save(); + FieldConfig::create([ + 'field_storage' => $media_required_field_storage, + '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_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->nodeTypeId = $node_type->id(); + $field_storage = FieldStorageConfig::create([ + 'type' => 'entity_reference', + 'cardinality' => -1, + 'entity_type' => 'node', + 'field_name' => 'field_media_reference', + 'settings' => [ + 'target_type' => 'media', + ], + ]); + $field_storage->save(); + FieldConfig::create([ + 'field_storage' => $field_storage, + 'bundle' => $this->nodeTypeId, + 'settings' => [ + 'handler_settings' => [ + 'target_bundles' => [ + $this->mediaTypeId => $this->mediaTypeId, + ], + ], + ], + ])->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); + } + + } + + /** + * Tests the file widget behavior. + */ + public function testInlineFileWidget() { + $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 required 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); + // Since cardinality is -1, we should be allowed to upload another file now. + $assert_session->elementExists('css', 'input[name="files[field_media_reference_1][]"]'); + + // By now the media entity should already exist, but unpublished. + $filename1 = basename($this->testFilePaths[0]); + /** @var \Drupal\media\MediaInterface $media */ + $media = $this->container->get('entity_type.manager')->getStorage('media') + ->loadByProperties(['name' => $filename1]); + $media = reset($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); + } + +}