diff -u b/core/modules/media/src/Form/MediaInlineForm.php b/core/modules/media/src/Form/MediaInlineForm.php --- b/core/modules/media/src/Form/MediaInlineForm.php +++ b/core/modules/media/src/Form/MediaInlineForm.php @@ -8,6 +8,7 @@ use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Render\Element; +use Drupal\media\Plugin\Field\FieldWidget\MediaFileWidget; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -127,30 +128,23 @@ 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. + $delta = $entity_form['#delta']; + $field_name = $entity_form['#field_name']; + $field_value = $form_state->getValue([$field_name, $delta]); + if (!empty($field_value['fids'])) { + $fid = reset($field_value['fids']); + $media_form_key = 'file_' . $fid; if (empty($entity_form[$media_form_key]['form_wrapper']['form'])) { - continue; + return; } $media_form = $entity_form[$media_form_key]['form_wrapper']['form']; - /** @var \Drupal\media\MediaInterface $entity */ - $entity = $media_form['#entity']; + /** @var \Drupal\media\MediaInterface $media_entity */ + $media_entity = MediaFileWidget::getParentMedia($form_state, $fid); $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); + $this->buildEntity($media_form, $media_entity, $form_state); + $form_display = EntityFormDisplay::collectRenderDisplay($media_entity, $media_form['#form_mode']); + $form_display->validateFormValues($media_entity, $media_form, $form_state); + $media_entity->setValidationRequired(FALSE); foreach ($form_state->getErrors() as $name => $message) { // $name may be unknown in $form_state and @@ -159,9 +153,10 @@ $form_state->setError($triggering_element, $message); } - // If no validation errors are present, set the media back to Published. + // If no validation errors are present, publish and save the media. if (empty($form_state->getErrors())) { - $entity->setPublished()->save(); + $media_entity->setPublished()->save(); + MediaFileWidget::setParentMedia($form_state, $fid, $media_entity); } } } diff -u b/core/modules/media/src/Plugin/Field/FieldWidget/MediaFileWidget.php b/core/modules/media/src/Plugin/Field/FieldWidget/MediaFileWidget.php --- b/core/modules/media/src/Plugin/Field/FieldWidget/MediaFileWidget.php +++ b/core/modules/media/src/Plugin/Field/FieldWidget/MediaFileWidget.php @@ -13,6 +13,7 @@ use Drupal\media\Entity\MediaType; use Drupal\Component\Utility\NestedArray; use Drupal\Core\Link; +use Drupal\media\MediaInterface; use Drupal\media\Plugin\media\Source\File; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -51,23 +52,7 @@ } /** - * 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 @@ -84,7 +69,7 @@ // 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() + // @see \Drupal\file\Plugin\Field\FieldWidget\FileWidget::formElement() $real_field_definition = $this->fieldDefinition; $this->fieldDefinition = $source_field_definition; $element = parent::formElement($source_field_items, 0, $element, $form, $form_state); @@ -100,21 +85,25 @@ $element['#field_name'] = $this->fieldDefinition->getName(); $element['#weight'] = $delta; - // Accommodate multiple uploads, if field settings require it. + // Accommodate multiple uploads--if field settings require it AND this is + // the empty upload-field-only "value" provided by FileWidget. + // @see \Drupal\file\Plugin\Field\FieldWidget\FileWidget::formMultipleElements() $cardinality = $this->fieldDefinition->getFieldStorageDefinition()->getCardinality(); + $multiple = ($cardinality != 1 && $items[$delta]->isEmpty() && empty($items[$delta]->getValue()['fids'])); $element['#cardinality'] = $cardinality; - $element['#multiple'] = $cardinality != 1 ? TRUE : FALSE; - if ($cardinality != 1 && $cardinality != -1) { + $element['#multiple'] = $multiple; + if ($cardinality != 1 && $cardinality != -1 && $multiple) { $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]; + $item = $items->get($delta); + if (!$item->isEmpty() && !$form_state->has('media_entities')) { + $fid = $item->entity->getSource()->getSourceFieldValue($item->entity); + static::setParentMedia($form_state, $fid, $item->entity); } - // Finally, fill in any needed defaults that haven't already been set. - $element['#default_value'] = $items[$delta]->getValue() + static::getDefaultValues(); + // Return the default value, filling in an empty fids value if none exists. + $element['#default_value'] = $items[$delta]->getValue() + ['fids' => []]; return $element; } @@ -123,20 +112,20 @@ * {@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; + // Only validate saved media entities. + // @see submit() + $values = parent::massageFormValues($values, $form, $form_state); + foreach ($values as $delta => $value) { + $fid = isset($value['target_id']) ? $value['target_id'] : 0; + $media_entity = static::getParentMedia($form_state, $fid); + if ($media_entity && !$media_entity->isNew()) { + $values[$delta]['target_id'] = $media_entity->id(); + } + else { + unset($values[$delta]); } } - - return $new_values; + return $values; } /** @@ -147,80 +136,49 @@ $form_object = $form_state->getFormObject(); if (!$form_object instanceOf EntityFormInterface) { - throw new \LogicException('We are in bat country.'); + throw new \LogicException('MediaFileWidget::value() should only be used by entity forms.'); } + $host_entity = $form_object->getEntity(); - // 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) { + // If we have a target_id, it's a mid; we're loading a previously-saved + // media entity reference. Translate it to a fid for ManagedFile to use and + // store the media entity for later. + if (!empty($return['target_id'])) { + /** @var \Drupal\media\MediaInterface $media_entity */ + $media_entity = Media::load($return['target_id']); + $fid = $media_entity->getSource()->getSourceFieldValue($media_entity); + $return['fids'][] = $fid; + + static::setParentMedia($form_state, $fid, $media_entity); + } + // The user has just uploaded new file(s). In this case, create a temporary, + // unsaved media entity for each file, and store it. + else if (!empty($return['fids'])) { 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(); - } - } + $media_entity = static::getParentMedia($form_state, $fid); + if (empty($media_entity)) { + // This widget can only be used when there's a single target bundle, + // so 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']); + + /** @var \Drupal\media\MediaInterface $media_entity */ + $media_entity = Media::create([ + 'bundle' => $media_type, + 'uid' => \Drupal::currentUser()->id(), + 'langcode' => $host_entity->language()->getId(), + ]); + $media_entity->setUnpublished(); + $source_field = $media_entity->getSource() + ->getSourceFieldDefinition($media_entity->bundle->entity) + ->getName(); + $media_entity->set($source_field, $fid); - 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); + static::setParentMedia($form_state, $fid, $media_entity); } - $media_entity->save(); - } - else { - // Warn process() that this is a preexisting value. - $return['media_already_saved'] = TRUE; } - $return['mids'] = [$mid]; } - return $return; } @@ -230,82 +188,43 @@ 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() + // For readability, working with $value rather than $element['#value']. $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. + // MediaFileWidget::value() stored the parent media entity for this fid if + // a) the user 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; - } - } + $fid = count($value['fids']) === 1 ? reset($value['fids']) : FALSE; + /** @var MediaInterface $media_entity */ + $media_entity = static::getParentMedia($form_state, $fid); // $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); + if ($media_entity) { + // Show the media icon and name in the element ManagedFile created. + $form_element_name = 'file_' . $fid; - // @todo: entity query to be sure whether or not thumbnail image style - // actually exists. If not stick the user with the "original" image style. + /** @var \Drupal\image\ImageStyleInterface $image_style */ $element[$form_element_name]['icon'] = [ - '#theme' => 'image_style', - '#style_name' => 'thumbnail', + '#theme' => 'image', '#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) { + /** @var \Drupal\image\ImageStyleInterface $style */ + if ($style = \Drupal::entityTypeManager()->getStorage('image_style')->load('thumbnail')) { + $element[$form_element_name]['icon']['#theme'] = 'image_style'; + $element[$form_element_name]['icon']['#style_name'] = $style->getName(); + } + + if ($media_entity->isNew()) { // Create and add the media entity form. $form_mode = 'add_inline'; $media_form = [ '#entity' => $media_entity, '#form_mode' => $form_mode, - '#parents' => $form_element_parents, + '#parents' => array_merge($element['#parents'], [$form_element_name]), ]; $inline_form_handler = \Drupal::entityTypeManager() ->getHandler('media', $form_mode); @@ -334,63 +253,6 @@ } /** - * 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) { @@ -420,2 +282,49 @@ + /** + * Internal method to get stored media entities. + * + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The host form state, where we've stored the fid-to-media map. + * @param $fid + * The file id we're being asked about. If 'all', return the whole array. + * + * @return bool|mixed + */ + public static function getParentMedia(FormStateInterface $form_state, $fid = 'all') { + // @todo Debug this to use $form_state storage rather than tempstore. + // $media_entities = $form_state->get('media_entities'); + $tempstore = \Drupal::service('user.private_tempstore')->get('media'); + $media_entities = $tempstore->get('media_entities'); + if ($fid && is_array($media_entities)) { + if ($fid == 'all') { + return $media_entities; + } + if (array_key_exists($fid, $media_entities)) { + return $media_entities[$fid]; + } + } + return FALSE; + } + + /** + * Internal method to map fids to media entities. + * + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The host form state. + * @param $fid + * The fid of the source file. + * @param \Drupal\media\MediaInterface $media_entity + * The media entity that uses this file as a source. + */ + public static function setParentMedia(FormStateInterface $form_state, $fid, MediaInterface $media_entity) { + // @todo Debug this to use $form_state storage rather than tempstore. + // $media_entities = $form_state->get('media_entities') ?: []; + // $media_entities[$fid] = $media_entity; + // $form_state->set('media_entities', $media_entities); + $tempstore = \Drupal::service('user.private_tempstore')->get('media'); + $media_entities = $tempstore->get('media_entities'); + $media_entities[$fid] = $media_entity; + $tempstore->set('media_entities', $media_entities); + } + } diff -u b/core/modules/media/tests/src/FunctionalJavascript/MediaInlineFileWidgetTest.php b/core/modules/media/tests/src/FunctionalJavascript/MediaInlineFileWidgetTest.php --- b/core/modules/media/tests/src/FunctionalJavascript/MediaInlineFileWidgetTest.php +++ b/core/modules/media/tests/src/FunctionalJavascript/MediaInlineFileWidgetTest.php @@ -13,7 +13,7 @@ /** * {@inheritdoc} */ - public static $modules = ['node']; + public static $modules = ['node', 'options']; protected $strictConfigSchema = FALSE; @@ -47,11 +47,14 @@ $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', + 'type' => 'list_string', 'entity_type' => 'media', 'field_name' => 'field_media_required', 'settings' => [ - 'max_length' => 255, + 'allowed_values' => [ + 'foo' => 'Foo', + 'bar' => 'Bar', + ], ], ]); $media_required_field_storage->save(); @@ -61,7 +64,7 @@ 'required' => TRUE, ])->save(); entity_get_form_display('media', $media_type->id(), 'add_inline') - ->setComponent('field_media_required', ['type' => 'string_textfield']) + ->setComponent('field_media_required', ['type' => 'options_select']) ->save(); $media_nonrequired_field_storage = FieldStorageConfig::create([ 'type' => 'string', @@ -89,7 +92,7 @@ 'settings' => [], ]) ->setComponent('field_media_required', [ - 'type' => 'string', + 'type' => 'list_default', 'label' => 'hidden', 'settings' => [], ]) @@ -165,7 +168,7 @@ $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); + $assert_session->elementExists('css', '.field--name-field-media-required select.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. @@ -183,7 +186,7 @@ // 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(); + $required_text = 'foo'; $page->fillField('field_media_required', $required_text); $page->pressButton('Save'); // The node has been correctly saved. @@ -221,7 +224,7 @@ $assert_session->assertWaitOnAjaxRequest(); // Fill in the second media required field and save. - $required_text2 = $this->randomMachineName(); + $required_text2 = 'bar'; $page->fillField('field_media_required', $required_text2); $page->pressButton('Save');