diff --git a/core/modules/image/config/schema/image.schema.yml b/core/modules/image/config/schema/image.schema.yml index 323220b..788a7cc 100644 --- a/core/modules/image/config/schema/image.schema.yml +++ b/core/modules/image/config/schema/image.schema.yml @@ -22,6 +22,9 @@ image.style.*: type: integer uuid: type: string + replacementID: + type: string + label: Replacement style ID image.effect.*: type: mapping diff --git a/core/modules/image/src/Entity/ImageStyle.php b/core/modules/image/src/Entity/ImageStyle.php index 7d97d59..10eba46 100644 --- a/core/modules/image/src/Entity/ImageStyle.php +++ b/core/modules/image/src/Entity/ImageStyle.php @@ -53,6 +53,7 @@ * "name", * "label", * "effects", + * "replacementID" * } * ) */ @@ -131,14 +132,6 @@ public static function postDelete(EntityStorageInterface $storage, array $entiti foreach ($entities as $style) { // Flush cached media for the deleted style. $style->flush(); - // Check whether field settings need to be updated. - // In case no replacement style was specified, all image fields that are - // using the deleted style are left in a broken state. - if (!$style->isSyncing() && $new_id = $style->getReplacementID()) { - // The deleted ID is still set as originalID. - $style->setName($new_id); - static::replaceImageStyle($style); - } } } diff --git a/core/modules/image/src/Form/ImageStyleDeleteForm.php b/core/modules/image/src/Form/ImageStyleDeleteForm.php index 5c45d94..ed4efc1 100644 --- a/core/modules/image/src/Form/ImageStyleDeleteForm.php +++ b/core/modules/image/src/Form/ImageStyleDeleteForm.php @@ -7,8 +7,12 @@ namespace Drupal\image\Form; +use Drupal\Core\Entity\Entity\EntityFormDisplay; +use Drupal\Core\Entity\Entity\EntityViewDisplay; use Drupal\Core\Entity\EntityDeleteForm; use Drupal\Core\Form\FormStateInterface; +use Drupal\field\Entity\FieldConfig; +use Drupal\image\Entity\ImageStyle; /** * Creates a form to delete an image style. @@ -16,29 +20,69 @@ class ImageStyleDeleteForm extends EntityDeleteForm { /** - * {@inheritdoc} + * A list of components using this image style. + * + * @var array[] */ - public function getQuestion() { - return $this->t('Optionally select a style before deleting %style', array('%style' => $this->entity->label())); - } + protected $affectedComponents; + /** * {@inheritdoc} + * + * @todo Convert the response to a nice rendered array once + * https://www.drupal.org/node/2575375 gets committed. */ public function getDescription() { - return $this->t('If this style is in use on the site, you may select another style to replace it. All images that have been generated for this style will be permanently deleted.'); + $description = ''; + // There are components relying on the style being deleted. + if ($this->getAffectedComponents()) { + $styles = array_keys(ImageStyle::loadMultiple()); + + // If there are image styles defined, other than the style being deleted, + // show the replacement description. + if (count($styles) > 1) { + $description .= '

' . $this->t("Next components are relying on %style image style. You may select another style to replace it in all components settings. If you don't provide a replacement the components will be disabled. In this case you'll need to revisit the form and view displays an reconfigure the widgets and formatters.", ['%style' => $this->entity->label()]) . '

'; + } + // No possible replacements, just announce the user that he needs to + // update its formatters and widgets. + else { + $description .= '

' . $this->t("Next components are relying on %style image style and will be disabled. You'll need to revisit the form and view displays an reconfigure the widgets and formatters.", ['%style' => $this->entity->label()]) . '

'; + } + + foreach ($this->getAffectedComponents() as $context => $info) { + if ($info['components']) { + //$description .= '

' . $this->t("@label:", ['@label' => $info['label']]) . '

'; + $items = ['#theme' => 'item_list', '#items' => $info['components'], '#title' => $this->t("@label:", ['@label' => $info['label']])]; + $description .= \Drupal::service('renderer')->render($items); + } + } + } + + $description .= '

' . $this->t("All images that have been generated for this style will be permanently deleted.") . '

'; + + return $description; } /** * {@inheritdoc} */ public function form(array $form, FormStateInterface $form_state) { - $replacement_styles = array_diff_key(image_style_options(), array($this->entity->id() => '')); - $form['replacement'] = array( - '#title' => $this->t('Replacement style'), - '#type' => 'select', - '#options' => $replacement_styles, - '#empty_option' => $this->t('No replacement, just delete'), - ); + // Show replacement element only if there are components relying on this + // image style. + if ($this->getAffectedComponents()) { + $replacement_styles = array_diff_key(image_style_options(), array($this->entity->id() => '')); + + // If there are non-empty options in the list, allow the user to + // optionally pickup a replacement. + if (count($replacement_styles) > 1) { + $form['replacement'] = array( + '#title' => $this->t('Replacement style'), + '#type' => 'select', + '#options' => $replacement_styles, + '#empty_option' => $this->t('No replacement, disable widgets and formatters using it'), + ); + } + } return parent::form($form, $form_state); } @@ -47,9 +91,59 @@ public function form(array $form, FormStateInterface $form_state) { * {@inheritdoc} */ public function submitForm(array &$form, FormStateInterface $form_state) { - $this->entity->set('replacementID', $form_state->getValue('replacement')); - + // If a replacement has been selected, save it in the entity to be used + // later, when resolving dependencies. + if (!empty($replacement = $form_state->getValue('replacement'))) { + ImageStyle::load($this->entity->id()) + ->set('replacementID', $replacement) + ->save(); + } parent::submitForm($form, $form_state); } + /** + * Returns a list of components that are using the image style being deleted. + * + * @return array[] + * An array with the keys 'view' and 'form' each one being an array of + * components that are using this image style in their settings. + */ + protected function getAffectedComponents() { + if (!isset($this->affectedComponents)) { + $this->affectedComponents = []; + // Merge view and form displays together. Use array_values() to avoid key + // collisions. + $displays = array_merge(array_values(EntityViewDisplay::loadMultiple()), array_values(EntityFormDisplay::loadMultiple())); + /** @var \Drupal\Core\Entity\Display\EntityDisplayInterface $display */ + foreach ($displays as $display) { + foreach ($display->getComponents() as $name => $options) { + $context = $display->get('displayContext'); + $component_type = $context == 'view' ? 'image' : 'image_image'; + $setting = $context == 'view' ? 'image_style' : 'preview_image_style'; + if (isset($options['type']) && $options['type'] == $component_type && $options['settings'][$setting] == $this->entity->id()) { + if (!isset($this->affectedComponents[$context])) { + $this->affectedComponents[$context] = [ + 'label' => (string) ($context == 'view' ? $this->t('Formatters') : $this->t('Widgets')), + 'components' => [], + ]; + } + /** @var \Drupal\field\FieldConfigInterface $field_config */ + $field_config = FieldConfig::load("{$display->getTargetEntityTypeId()}.{$display->getTargetBundle()}.$name"); + $entity_type = $this->entityManager->getDefinition($field_config->getTargetEntityTypeId()); + $entity_type_label = $entity_type->getLabel(); + if ($bundle_entity_type_id = $entity_type->getBundleEntityType()) { + $bundle_label = $this->entityManager->getStorage($bundle_entity_type_id) + ->load($field_config->getTargetBundle())->label(); + } + else { + $bundle_label = $field_config->getTargetBundle(); + } + $this->affectedComponents[$context]['components'][$name] = (string) $this->t("@entity: @bundle: @field", ['@entity' => $entity_type_label, '@bundle' => $bundle_label, '@field' => $field_config->getLabel()]); + } + } + } + } + return $this->affectedComponents; + } + } diff --git a/core/modules/image/src/Plugin/Field/FieldFormatter/ImageFormatter.php b/core/modules/image/src/Plugin/Field/FieldFormatter/ImageFormatter.php index 3069721..1d47b75 100644 --- a/core/modules/image/src/Plugin/Field/FieldFormatter/ImageFormatter.php +++ b/core/modules/image/src/Plugin/Field/FieldFormatter/ImageFormatter.php @@ -12,10 +12,9 @@ use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Link; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; -use Drupal\Core\Routing\UrlGeneratorInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\Url; -use Drupal\Core\Utility\LinkGeneratorInterface; +use Drupal\image\Entity\ImageStyle; use Symfony\Component\DependencyInjection\ContainerInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Cache\Cache; @@ -228,4 +227,39 @@ public function viewElements(FieldItemListInterface $items) { return $elements; } + /** + * {@inheritdoc} + */ + public function calculateDependencies() { + $dependencies = parent::calculateDependencies(); + /** @var \Drupal\image\ImageStyleInterface $style */ + $style_id = $this->getSetting('image_style'); + if ($style_id && $style = ImageStyle::load($style_id)) { + $dependencies[$style->getConfigDependencyKey()][] = $style->getConfigDependencyName(); + } + return $dependencies; + } + + /** + * {@inheritdoc} + */ + public function onDependencyRemoval(array $dependencies) { + $changed = parent::onDependencyRemoval($dependencies); + /** @var \Drupal\image\ImageStyleInterface $style */ + $name = $this->getSetting('image_style'); + if ($name && $style = ImageStyle::load($name)) { + /** @var \Drupal\image\ImageStyleInterface $removed_style */ + if (!empty($removed_style = $dependencies[$style->getConfigDependencyKey()][$style->getConfigDependencyName()])) { + if ($replacement_id = $removed_style->getReplacementID()) { + /** @var \Drupal\image\ImageStyleInterface $replacement */ + if ($replacement = ImageStyle::load($replacement_id)) { + $this->setSetting('image_style', $replacement_id); + $changed = TRUE; + } + } + } + } + return $changed; + } + } diff --git a/core/modules/image/src/Plugin/Field/FieldWidget/ImageWidget.php b/core/modules/image/src/Plugin/Field/FieldWidget/ImageWidget.php index 1449c7f..445ff2c 100644 --- a/core/modules/image/src/Plugin/Field/FieldWidget/ImageWidget.php +++ b/core/modules/image/src/Plugin/Field/FieldWidget/ImageWidget.php @@ -12,6 +12,7 @@ use Drupal\Core\Form\FormStateInterface; use Drupal\file\Entity\File; use Drupal\file\Plugin\Field\FieldWidget\FileWidget; +use Drupal\image\Entity\ImageStyle; /** * Plugin implementation of the 'image_image' widget. @@ -274,4 +275,39 @@ public static function validateRequiredFields($element, FormStateInterface $form } } + /** + * {@inheritdoc} + */ + public function calculateDependencies() { + $dependencies = parent::calculateDependencies(); + /** @var \Drupal\image\ImageStyleInterface $style */ + $style_id = $this->getSetting('preview_image_style'); + if ($style_id && $style = ImageStyle::load($style_id)) { + $dependencies[$style->getConfigDependencyKey()][] = $style->getConfigDependencyName(); + } + return $dependencies; + } + + /** + * {@inheritdoc} + */ + public function onDependencyRemoval(array $dependencies) { + $changed = parent::onDependencyRemoval($dependencies); + /** @var \Drupal\image\ImageStyleInterface $style */ + $name = $this->getSetting('preview_image_style'); + if ($name && $style = ImageStyle::load($name)) { + /** @var \Drupal\image\ImageStyleInterface $removed_style */ + if (!empty($removed_style = $dependencies[$style->getConfigDependencyKey()][$style->getConfigDependencyName()])) { + if ($replacement_id = $removed_style->getReplacementID()) { + /** @var \Drupal\image\ImageStyleInterface $replacement */ + if ($replacement = ImageStyle::load($replacement_id)) { + $this->setSetting('preview_image_style', $replacement_id); + $changed = TRUE; + } + } + } + } + return $changed; + } + } diff --git a/core/modules/image/src/Tests/ImageFieldTestBase.php b/core/modules/image/src/Tests/ImageFieldTestBase.php index 271b969..6e894f5 100644 --- a/core/modules/image/src/Tests/ImageFieldTestBase.php +++ b/core/modules/image/src/Tests/ImageFieldTestBase.php @@ -67,8 +67,10 @@ protected function setUp() { * A list of instance settings that will be added to the instance defaults. * @param array $widget_settings * A list of widget settings that will be added to the widget defaults. + * @param array $formatter_settings + * A list of formatter settings that will be added to the formatter defaults. */ - function createImageField($name, $type_name, $storage_settings = array(), $field_settings = array(), $widget_settings = array()) { + function createImageField($name, $type_name, $storage_settings = array(), $field_settings = array(), $widget_settings = array(), $formatter_settings = array()) { entity_create('field_storage_config', array( 'field_name' => $name, 'entity_type' => 'node', @@ -95,7 +97,10 @@ function createImageField($name, $type_name, $storage_settings = array(), $field ->save(); entity_get_display('node', $type_name, 'default') - ->setComponent($name) + ->setComponent($name, array( + 'type' => 'image', + 'settings' => $formatter_settings, + )) ->save(); return $field_config; diff --git a/core/modules/image/src/Tests/ImageStyleDeleteTest.php b/core/modules/image/src/Tests/ImageStyleDeleteTest.php new file mode 100644 index 0000000..d5df67d41 --- /dev/null +++ b/core/modules/image/src/Tests/ImageStyleDeleteTest.php @@ -0,0 +1,154 @@ +style1 = ImageStyle::create(['name' => 'style1', 'label' => 'Style 1']); + $this->style1->save(); + $this->style2 = ImageStyle::create(['name' => 'style2', 'label' => 'Style 2']); + $this->style2->save(); + $this->style3 = ImageStyle::create(['name' => 'style3', 'label' => 'Style 3']); + $this->style3->save(); + + $this->createImageField('image1', 'page', [], [], ['preview_image_style' => 'style1'], ['image_style' => 'style1']); + $this->createImageField('image2', 'article', [], [], ['preview_image_style' => 'style2']); + } + + /** + * Tests image style deletion messages. + */ + public function testDeletionMessages() { + $this->drupalGet('admin/config/media/image-styles/manage/style1/delete'); + // Replacement select found. + $this->assertFieldByName('replacement'); + // Message telling about components relying on this image style found. + $this->assertRaw((string) t("Next components are relying on %style image style. You may select another style to replace it in all components settings. If you don't provide a replacement the components will be disabled. In this case you'll need to revisit the form and view displays an reconfigure the widgets and formatters.", ['%style' => 'Style 1'])); + // Check listed components. + $this->assertText("Formatters"); + $this->assertText("Widgets"); + $this->assertText("Content: Basic page: image1"); + $this->assertNoText("Content: Article: image1"); + $this->assertNoText("Content: Article: image2"); + // The style cache flush text is there. + $this->assertText((string) t('All images that have been generated for this style will be permanently deleted.')); + + $this->drupalGet('admin/config/media/image-styles/manage/style2/delete'); + // Replacement select found. + $this->assertFieldByName('replacement'); + // Message telling about components relying on this image style found. + $this->assertRaw((string) t("Next components are relying on %style image style. You may select another style to replace it in all components settings. If you don't provide a replacement the components will be disabled. In this case you'll need to revisit the form and view displays an reconfigure the widgets and formatters.", ['%style' => 'Style 2'])); + // Check listed components. + $this->assertText("Widgets"); + $this->assertNoText("Formatters"); + $this->assertText("Content: Article: image2"); + $this->assertNoText("Content: Basic page: image1"); + $this->assertNoText("Content: Article: image1"); + // The style cache flush text is there. + $this->assertText((string) t('All images that have been generated for this style will be permanently deleted.')); + + $this->drupalGet('admin/config/media/image-styles/manage/style3/delete'); + // Replacement select not found. + $this->assertNoFieldByName('replacement'); + // Messages telling about components relying on this image style not found. + $this->assertNoRaw((string) t("Next components are relying on %style image style. You may select another style to replace it in all components settings. If you don't provide a replacement the components will be disabled. In this case you'll need to revisit the form and view displays an reconfigure the widgets and formatters.", ['%style' => 'Style 3'])); + $this->assertNoRaw((string) t("Next components are relying on %style image style and will be disabled. You'll need to revisit the form and view displays an reconfigure the widgets and formatters.", ['%style' => 'Style 3'])); + // There are no listed components. + $this->assertNoText("Formatters"); + $this->assertNoText("Widgets"); + // The style cache flush text is there. + $this->assertText((string) t('All images that have been generated for this style will be permanently deleted.')); + + // Delete all styles except 'style1'. + foreach (ImageStyle::loadMultiple() as $image_style) { + if ($image_style->id() != 'style1') { + $image_style->delete(); + } + } + + $this->drupalGet('admin/config/media/image-styles/manage/style1/delete'); + // Replacement select not found. + $this->assertNoFieldByName('replacement'); + // Message telling about components relying on this image style found. + $this->assertRaw((string) t("Next components are relying on %style image style and will be disabled. You'll need to revisit the form and view displays an reconfigure the widgets and formatters.", ['%style' => 'Style 1'])); + // Check listed components. + $this->assertText("Formatters"); + $this->assertText("Widgets"); + $this->assertText("Content: Basic page: image1"); + $this->assertNoText("Content: Article: image1"); + $this->assertNoText("Content: Article: image2"); + // The style cache flush text is there. + $this->assertText((string) t('All images that have been generated for this style will be permanently deleted.')); + } + + /** + * Tests the deletion of image styles. + */ + public function testDelete() { + // Delete by assuring a replacement. + $edit = ['replacement' => 'style2']; + $this->drupalPostForm('admin/config/media/image-styles/manage/style1/delete', $edit, (string) t('Delete')); + + $view_display = EntityViewDisplay::load("node.page.default"); + // Formatter setting should have been replaced. + if ($this->assertNotNull($component = $view_display->getComponent('image1'))) { + $this->assertIdentical($component['settings']['image_style'], 'style2'); + } + // Widget setting should have been replaced. + $form_display = EntityFormDisplay::load("node.page.default"); + if ($this->assertNotNull($component = $form_display->getComponent('image1'))) { + $this->assertIdentical($component['settings']['preview_image_style'], 'style2'); + } + + // Delete without assuring a replacement. + $this->drupalPostForm('admin/config/media/image-styles/manage/style2/delete', [], (string) t('Delete')); + $view_display = EntityViewDisplay::load("node.page.default"); + // Formatter setting should have been disabled. + $this->assertNull($view_display->getComponent('image1')); + $this->assertNotNull($view_display->get('hidden')['image1']); + // Widget setting should have been disabled. + $form_display = EntityFormDisplay::load("node.page.default"); + $this->assertNull($form_display->getComponent('image1')); + $this->assertNotNull($form_display->get('hidden')['image1']); + } + +} \ No newline at end of file diff --git a/core/modules/image/tests/src/Kernel/ImageStyleIntegrationTest.php b/core/modules/image/tests/src/Kernel/ImageStyleIntegrationTest.php new file mode 100644 index 0000000..f1a0deb --- /dev/null +++ b/core/modules/image/tests/src/Kernel/ImageStyleIntegrationTest.php @@ -0,0 +1,114 @@ + 'main_style']); + $style->save(); + /** @var \Drupal\image\ImageStyleInterface $replacement */ + $replacement = ImageStyle::create(['name' => 'replacement_style']); + $replacement->save(); + + // Create a node-type, named 'note'. + $node_type = NodeType::create(['type' => 'note']); + $node_type->save(); + + // Create an image field and attach it to the 'note' node-type. + FieldStorageConfig::create([ + 'entity_type' => 'node', + 'field_name' => 'sticker', + 'type' => 'image', + ])->save(); + FieldConfig::create([ + 'entity_type' => 'node', + 'field_name' => 'sticker', + 'bundle' => 'note', + ])->save(); + + // Create the default entity view display and set the 'sticker' field to use + // the 'main_style' images style in formatter. + /** @var \Drupal\Core\Entity\Display\EntityViewDisplayInterface $view_display */ + $view_display = EntityViewDisplay::create([ + 'targetEntityType' => 'node', + 'bundle' => 'note', + 'mode' => 'default', + 'status' => TRUE, + ])->setComponent('sticker', ['settings' => ['image_style' => 'main_style']]); + $view_display->save(); + + // Create the default entity form display and set the 'sticker' field to use + // the 'main_style' images style in the widget. + /** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $form_display */ + $form_display = EntityFormDisplay::create([ + 'targetEntityType' => 'node', + 'bundle' => 'note', + 'mode' => 'default', + 'status' => TRUE, + ])->setComponent('sticker', ['settings' => ['preview_image_style' => 'main_style']]); + $form_display->save(); + + // The entity view and form displays exists before dependency removal. + $this->assertNotNull(EntityViewDisplay::load($view_display->id())); + $this->assertNotNull(EntityFormDisplay::load($form_display->id())); + + // Delete the 'main_style' but before emulate the UI selecting of + // replacement image style by simply setting the property and saving. + $style + ->set('replacementID', 'replacement_style') + ->save(); + $style->delete(); + + // The entity view and form displays exists after dependency removal. + $this->assertNotNull($view_display = EntityViewDisplay::load($view_display->id())); + $this->assertNotNull($form_display = EntityFormDisplay::load($form_display->id())); + // The 'sticker' formatter component exists in both displays. + $this->assertNotNull($formatter = $view_display->getComponent('sticker')); + $this->assertNotNull($widget = $form_display->getComponent('sticker')); + // Both displays are using now 'replacement_style' to show images. + $this->assertSame($formatter['settings']['image_style'], 'replacement_style'); + $this->assertSame($widget['settings']['preview_image_style'], 'replacement_style'); + + // Delete the 'replacement_style' without setting a replacement image style. + $replacement->delete(); + + // The entity view and form displays exists after dependency removal. + $this->assertNotNull($view_display = EntityViewDisplay::load($view_display->id())); + $this->assertNotNull($form_display = EntityFormDisplay::load($form_display->id())); + // The 'sticker' formatter component should be hidden in both displays. + $this->assertNull($view_display->getComponent('sticker')); + $this->assertTrue($view_display->get('hidden')['sticker']); + $this->assertNull($form_display->getComponent('sticker')); + $this->assertTrue($form_display->get('hidden')['sticker']); + } + +}