diff --git a/media_entity.module b/media_entity.module index d54f773..9b157e5 100644 --- a/media_entity.module +++ b/media_entity.module @@ -6,6 +6,7 @@ */ use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\media_entity\Entity\MediaBundle; /** * Implements hook_help(). diff --git a/src/Entity/MediaBundle.php b/src/Entity/MediaBundle.php index 8833f82..e7d3295 100644 --- a/src/Entity/MediaBundle.php +++ b/src/Entity/MediaBundle.php @@ -2,12 +2,16 @@ namespace Drupal\media_entity\Entity; -use Drupal\Core\Entity\EntityDescriptionInterface; use Drupal\Core\Config\Entity\ConfigEntityBundleBase; +use Drupal\Core\Entity\Entity\EntityFormDisplay; +use Drupal\Core\Entity\Entity\EntityViewDisplay; +use Drupal\Core\Entity\EntityDescriptionInterface; +use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\EntityWithPluginCollectionInterface; use Drupal\Core\Plugin\DefaultSingleLazyPluginCollection; use Drupal\media_entity\MediaBundleInterface; use Drupal\media_entity\MediaInterface; +use Drupal\field\Entity\FieldConfig; /** * Defines the Media bundle configuration entity. @@ -44,6 +48,7 @@ use Drupal\media_entity\MediaInterface; * "type_configuration", * "field_map", * "status", + * "create_source_field", * }, * links = { * "add-form" = "/admin/structure/media/add", @@ -121,7 +126,7 @@ class MediaBundle extends ConfigEntityBundleBase implements MediaBundleInterface /** * Default status of this media bundle. * - * @var array + * @var bool */ public $status = TRUE; @@ -241,4 +246,72 @@ class MediaBundle extends ConfigEntityBundleBase implements MediaBundleInterface $this->new_revision = $new_revision; } + /** + * {@inheritdoc} + */ + public function postSave(EntityStorageInterface $storage, $update = TRUE) { + parent::postSave($storage, $update); + + if (!$update) { + // When creating new bundles, add also a source field, if configured to + // do so. + $media_type_manager = \Drupal::service('plugin.manager.media_entity.type'); + /** @var \Drupal\media_entity\MediaTypeInterface $instance */ + $instance = $media_type_manager->createInstance($this->getType()->getPluginId(), $this->getTypeConfiguration()); + $configuration = $instance->getConfiguration(); + + $source_field_info = $instance->getDefaultSourceField(); + + if (!$this->isSyncing() && !empty($source_field_info)) { + + foreach ($source_field_info as $source_key => $field_details) { + // Save the field storage and create the instance. + $field_details['storage']->save(); + FieldConfig::create([ + 'entity_type' => 'media', + 'field_name' => $field_details['field_name'], + 'label' => $field_details['label'], + 'required' => TRUE, + 'bundle' => $this->id(), + ])->save(); + + // Make the field visible on the form display. + /** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $form_display */ + $form_display = EntityFormDisplay::create([ + 'targetEntityType' => 'media', + 'bundle' => $this->id(), + 'mode' => 'default', + 'status' => TRUE, + ]); + $form_display->setComponent($field_details['field_name'], [ + 'type' => $field_details['widget'], + ])->save(); + + // Make the field visible on the media entity itself. + /** @var \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display */ + $display = EntityViewDisplay::create([ + 'targetEntityType' => 'media', + 'bundle' => $this->id(), + 'mode' => 'default', + 'status' => TRUE, + ]); + $display->setComponent($field_details['field_name'], [ + 'type' => $field_details['formatter'], + ])->save(); + + $configuration[$source_key] = $field_details['field_name']; + } + + // Save the source field(s) name(s) to the plugin type configuration. + \Drupal::configFactory() + ->getEditable('media_entity.bundle.' . $this->id()) + ->set('type_configuration', $configuration) + ->save(); + + } + + } + + } + } diff --git a/src/MediaBundleForm.php b/src/MediaBundleForm.php index 36365f3..3a0d121 100644 --- a/src/MediaBundleForm.php +++ b/src/MediaBundleForm.php @@ -313,6 +313,7 @@ class MediaBundleForm extends EntityForm { $plugin_configuration = !empty($this->configurableInstances[$plugin]['plugin_config']) ? $this->configurableInstances[$plugin]['plugin_config'] : []; $instance = $this->mediaTypeManager->createInstance($plugin, $plugin_configuration); $instance->submitConfigurationForm($form, $form_state); + } /** diff --git a/src/MediaTypeBase.php b/src/MediaTypeBase.php index a472e7d..0041851 100644 --- a/src/MediaTypeBase.php +++ b/src/MediaTypeBase.php @@ -3,14 +3,14 @@ namespace Drupal\media_entity; use Drupal\Component\Plugin\PluginBase; +use Drupal\Component\Utility\NestedArray; use Drupal\Core\Config\Config; use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; -use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; -use Drupal\Component\Utility\NestedArray; +use Drupal\Core\StringTranslation\StringTranslationTrait; use Symfony\Component\DependencyInjection\ContainerInterface; -use Drupal\Core\Form\FormStateInterface; /** * Base implementation of media type plugin. @@ -158,4 +158,11 @@ abstract class MediaTypeBase extends PluginBase implements MediaTypeInterface, C return 'media:' . $media->bundle() . ':' . $media->uuid(); } + /** + * {@inheritdoc} + */ + public function getDefaultSourceField() { + return []; + } + } diff --git a/src/MediaTypeInterface.php b/src/MediaTypeInterface.php index 2a171c2..47e6994 100644 --- a/src/MediaTypeInterface.php +++ b/src/MediaTypeInterface.php @@ -2,8 +2,8 @@ namespace Drupal\media_entity; -use Drupal\Component\Plugin\PluginInspectionInterface; use Drupal\Component\Plugin\ConfigurablePluginInterface; +use Drupal\Component\Plugin\PluginInspectionInterface; use Drupal\Core\Plugin\PluginFormInterface; /** @@ -85,4 +85,32 @@ interface MediaTypeInterface extends PluginInspectionInterface, ConfigurablePlug */ public function getDefaultName(MediaInterface $media); + /** + * Provide a default source field. + * + * Plugins defining media bundles are required to implement this method and + * create at least one (unsaved) field that will be used as default source + * field(s), on bundle creation. + * + * @return array + * An associative array of fields that this plugin considers as source + * fields, keyed by the string used in the configuration form to reference + * this/these field(s). In a typical scenario, the array will contain only + * one value, and this first-level key will likely be "source_field". Other + * plugins could refer to their source fields with keys such as + * "main_source_field" + "secondary_source_field" or anything similar. + * Each value of this array will also be an associative array with the + * following structure: + * 'storage' => \Drupal\field\Entity\FieldStorageConfig An unsaved config + * entity for this field storage. + * 'field_name' => (string) The name to be used for the field instance. + * 'label' => (string) The label to be used for the field instance. + * 'widget' => (string) The id of the widget to use. + * 'formatter' => (string) The id of the formatter to use. + * + * Note that the storage config entity need to be returned unsaved, and the + * main form will be responsible for saving it when processing the submit. + */ + public function getDefaultSourceField(); + } diff --git a/tests/modules/media_entity_test_type/src/Plugin/MediaEntity/Type/TestType.php b/tests/modules/media_entity_test_type/src/Plugin/MediaEntity/Type/TestType.php index 60d68a0..a2c5049 100644 --- a/tests/modules/media_entity_test_type/src/Plugin/MediaEntity/Type/TestType.php +++ b/tests/modules/media_entity_test_type/src/Plugin/MediaEntity/Type/TestType.php @@ -3,6 +3,7 @@ namespace Drupal\media_entity_test_type\Plugin\MediaEntity\Type; use Drupal\Core\Form\FormStateInterface; +use Drupal\field\Entity\FieldStorageConfig; use Drupal\media_entity\Plugin\MediaEntity\Type\Generic; /** @@ -17,6 +18,21 @@ use Drupal\media_entity\Plugin\MediaEntity\Type\Generic; class TestType extends Generic { /** + * The name of the default source field on the media entity. + */ + const MEDIA_ENTITY_TEST_TYPE_DEFAULT_FIELD_NAME = 'field_media_test_type'; + + /** + * The id of the widget to be used when creating the default source field. + */ + const MEDIA_ENTITY_TEST_TYPE_DEFAULT_FIELD_WIDGET = 'string_textfield'; + + /** + * The id of the formatter to be used when creating the default source field. + */ + const MEDIA_ENTITY_TEST_TYPE_DEFAULT_FIELD_FORMATTER = 'string'; + + /** * {@inheritdoc} */ public function providedFields() { @@ -32,6 +48,7 @@ class TestType extends Generic { public function defaultConfiguration() { return [ 'test_config_value' => 'This is default value.', + 'create_source_field' => TRUE, ]; } @@ -45,7 +62,56 @@ class TestType extends Generic { '#default_value' => empty($this->configuration['test_config_value']) ? NULL : $this->configuration['test_config_value'], ]; + $options = []; + $allowed_field_types = ['string']; + /** @var \Drupal\media_entity\MediaBundleInterface $bundle */ + $bundle = $form_state->getFormObject()->getEntity(); + foreach ($this->entityFieldManager->getFieldDefinitions('media', $bundle->id()) as $field_name => $field) { + if (in_array($field->getType(), $allowed_field_types) && !$field->getFieldStorageDefinition()->isBaseField()) { + $options[$field_name] = $field->getLabel(); + } + } + + $form['source_field'] = array( + '#type' => 'select', + '#title' => $this->t('Field with source information'), + '#description' => $this->t('Field on media entity that stores the source information. You can create a bundle without selecting a value for this dropdown initially. This dropdown can be populated after adding fields to the bundle.'), + '#default_value' => empty($this->configuration['source_field']) ? NULL : $this->configuration['source_field'], + '#options' => $options, + ); + + // Add a checkbox to allow the field being created automatically on save. + if (empty($this->configuration['source_field'])) { + $form['create_source_field'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Create a source field automatically when saving this form.'), + '#description' => $this->t('If checked, a default field will be created and used as a source field. You can change this setting later.'), + '#default_value' => $this->configuration['create_source_field'], + '#access' => $bundle->isNew(), + ]; + } + return $form; } + /** + * {@inheritdoc} + */ + public function getDefaultSourceField() { + if (!$storage = FieldStorageConfig::loadByName('media', static::MEDIA_ENTITY_TEST_TYPE_DEFAULT_FIELD_NAME)) { + $storage = FieldStorageConfig::create([ + 'field_name' => static::MEDIA_ENTITY_TEST_TYPE_DEFAULT_FIELD_NAME, + 'entity_type' => 'media', + 'type' => 'string', + ]); + } + return [ + 'storage' => $storage, + 'field_name' => static::MEDIA_ENTITY_TEST_TYPE_DEFAULT_FIELD_NAME, + 'label' => $this->t('Test source field'), + 'field_widget' => static::MEDIA_ENTITY_TEST_TYPE_DEFAULT_FIELD_WIDGET, + 'field_formatter' => static::MEDIA_ENTITY_TEST_TYPE_DEFAULT_FIELD_FORMATTER, + ]; + } + } diff --git a/tests/src/FunctionalJavascript/BundleCreationTest.php b/tests/src/FunctionalJavascript/BundleCreationTest.php new file mode 100644 index 0000000..7483065 --- /dev/null +++ b/tests/src/FunctionalJavascript/BundleCreationTest.php @@ -0,0 +1,55 @@ +getSession()->getPage(); + + $this->drupalGet('admin/structure/media/add'); + + // Fill in a label to the bundle. + $page->fillField('label', 'Foo bundle'); + + // Select our test bundle type. + $this->assertSession()->fieldExists('type'); + $this->assertSession()->optionExists('type', 'test_type'); + $page->selectFieldOption('type', 'test_type'); + $this->waitForAjaxToFinish(); + + // Make sure the checkbox for creating the source field is there and save. + $this->assertSession()->checkboxChecked('type_configuration[test_type][create_source_field]'); + $page->pressButton('Save media bundle'); + + // Check whether the source field was correctly created. + // @TODO Finish me. + + } + +} diff --git a/tests/src/FunctionalJavascript/MediaEntityJavascriptTestBase.php b/tests/src/FunctionalJavascript/MediaEntityJavascriptTestBase.php new file mode 100644 index 0000000..b47a672 --- /dev/null +++ b/tests/src/FunctionalJavascript/MediaEntityJavascriptTestBase.php @@ -0,0 +1,91 @@ +drupalCreateUser([ + 'administer media', + 'administer media bundles', + // Media entity permissions. + 'view media', + 'create media', + 'update media', + 'update any media', + 'delete media', + 'delete any media', + 'access media overview', + ]); + $this->drupalLogin($account); + } + + /** + * Waits for jQuery to become ready and animations to complete. + */ + protected function waitForAjaxToFinish() { + $this->assertSession()->assertWaitOnAjaxRequest(); + } + + /** + * Waits and asserts that a given element is visible. + * + * @param string $selector + * The CSS selector. + * @param int $timeout + * (Optional) Timeout in milliseconds, defaults to 1000. + * @param string $message + * (Optional) Message to pass to assertJsCondition(). + */ + protected function waitUntilVisible($selector, $timeout = 1000, $message = '') { + $condition = "jQuery('" . $selector . ":visible').length > 0"; + $this->assertJsCondition($condition, $timeout, $message); + } + + /** + * Debugger method to save additional HTML output. + * + * The base class will only save browser output when accessing page using + * ::drupalGet and providing a printer class to PHPUnit. This method + * is intended for developers to help debug browser test failures and capture + * more verbose output. + */ + protected function saveHtmlOutput() { + $out = $this->getSession()->getPage()->getContent(); + // Ensure that any changes to variables in the other thread are picked up. + $this->refreshVariables(); + if ($this->htmlOutputEnabled) { + $html_output = '
Ending URL: ' . $this->getSession()->getCurrentUrl(); + $html_output .= '
' . $out; + $html_output .= $this->getHtmlOutputHeaders(); + $this->htmlOutput($html_output); + } + } + +}