Commit f7520a29 authored by alexpott's avatar alexpott

Issue #2468045 by Lendude, vaplas, geertvd, tim.plunkett, dawehner, xjm,...

Issue #2468045 by Lendude, vaplas, geertvd, tim.plunkett, dawehner, xjm, cilefen, catch, Berdir, alexpott: When deleting a content type field, users do not realize the related View also is deleted
parent e4e32ed6
......@@ -5,6 +5,7 @@
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\simpletest\WebTestBase;
use Drupal\views\Entity\View;
use Drupal\views\Tests\ViewTestData;
/**
......@@ -74,6 +75,13 @@ public function testDeleteField() {
\Drupal::service('module_installer')->install(['views']);
ViewTestData::createTestViews(get_class($this), ['field_test_views']);
$view = View::load('test_view_field_delete');
$this->assertNotNull($view);
$this->assertTrue($view->status());
// Test that the View depends on the field.
$dependencies = $view->getDependencies() + ['config' => []];
$this->assertTrue(in_array("field.storage.node.$field_name", $dependencies['config']));
// Check the config dependencies of the first field, the field storage must
// not be shown as being deleted yet.
$this->drupalGet("$bundle_path1/fields/node.$type_name1.$field_name/delete");
......@@ -91,13 +99,13 @@ public function testDeleteField() {
// Check the config dependencies of the first field.
$this->drupalGet("$bundle_path2/fields/node.$type_name2.$field_name/delete");
$this->assertText(t('The listed configuration will be deleted.'));
$this->assertText(t('The listed configuration will be updated.'));
$this->assertText(t('View'));
$this->assertText('test_view_field_delete');
$xml = $this->cssSelect('#edit-entity-deletes');
// Remove the wrapping HTML.
$this->assertIdentical(FALSE, strpos($xml[0]->asXml(), $field_label), 'The currently being deleted field is not shown in the entity deletions.');
// Test that nothing is scheduled for deletion.
$this->assertFalse(isset($xml[0]), 'The field currently being deleted is not shown in the entity deletions.');
// Delete the second field.
$this->fieldUIDeleteField($bundle_path2, "node.$type_name2.$field_name", $field_label, $type_name2);
......@@ -106,6 +114,14 @@ public function testDeleteField() {
$this->assertNull(FieldConfig::loadByName('node', $type_name2, $field_name), 'Field was deleted.');
// Check that the field storage was deleted too.
$this->assertNull(FieldStorageConfig::loadByName('node', $field_name), 'Field storage was deleted.');
// Test that the View isn't deleted and has been disabled.
$view = View::load('test_view_field_delete');
$this->assertNotNull($view);
$this->assertFalse($view->status());
// Test that the View no longer depends on the deleted field.
$dependencies = $view->getDependencies() + ['config' => []];
$this->assertFalse(in_array("field.storage.node.$field_name", $dependencies['config']));
}
}
......@@ -9,6 +9,7 @@
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\views\Plugin\DependentWithRemovalPluginInterface;
use Drupal\views\Views;
use Drupal\views\ViewEntityInterface;
......@@ -512,4 +513,61 @@ public function invalidateCaches() {
\Drupal::service('cache_tags.invalidator')->invalidateTags($tags);
}
/**
* {@inheritdoc}
*/
public function onDependencyRemoval(array $dependencies) {
$changed = FALSE;
// Don't intervene if the views module is removed.
if (isset($dependencies['module']) && in_array('views', $dependencies['module'])) {
return FALSE;
}
// If the base table for the View is provided by a module being removed, we
// delete the View because this is not something that can be fixed manually.
$views_data = Views::viewsData();
$base_table = $this->get('base_table');
$base_table_data = $views_data->get($base_table);
if (!empty($base_table_data['table']['provider']) && in_array($base_table_data['table']['provider'], $dependencies['module'])) {
return FALSE;
}
$current_display = $this->getExecutable()->current_display;
$handler_types = Views::getHandlerTypes();
// Find all the handlers and check whether they want to do something on
// dependency removal.
foreach ($this->display as $display_id => $display_plugin_base) {
$this->getExecutable()->setDisplay($display_id);
$display = $this->getExecutable()->getDisplay();
foreach (array_keys($handler_types) as $handler_type) {
$handlers = $display->getHandlers($handler_type);
foreach ($handlers as $handler_id => $handler) {
if ($handler instanceof DependentWithRemovalPluginInterface) {
if ($handler->onDependencyRemoval($dependencies)) {
// Remove the handler and indicate we made changes.
unset($this->display[$display_id]['display_options'][$handler_types[$handler_type]['plural']][$handler_id]);
$changed = TRUE;
}
}
}
}
}
// Disable the View if we made changes.
// @todo https://www.drupal.org/node/2832558 Give better feedback for
// disabled config.
if ($changed) {
// Force a recalculation of the dependencies if we made changes.
$this->getExecutable()->current_display = NULL;
$this->calculateDependencies();
$this->disable();
}
$this->getExecutable()->setDisplay($current_display);
return $changed;
}
}
<?php
namespace Drupal\views\Plugin;
/**
* Provides an interface for a plugin that has dependencies that can be removed.
*
* @ingroup views_plugins
*/
interface DependentWithRemovalPluginInterface {
/**
* Allows a plugin to define whether it should be removed.
*
* If this method returns TRUE then the plugin should be removed.
*
* @param array $dependencies
* An array of dependencies that will be deleted keyed by dependency type.
* Dependency types are, for example, entity, module and theme.
*
* @return bool
* TRUE if the plugin instance should be removed.
*
* @see \Drupal\Core\Config\Entity\ConfigDependencyManager
* @see \Drupal\Core\Config\ConfigEntityBase::preDelete()
* @see \Drupal\Core\Config\ConfigManager::uninstall()
* @see \Drupal\Core\Entity\EntityDisplayBase::onDependencyRemoval()
*/
public function onDependencyRemoval(array $dependencies);
}
......@@ -23,6 +23,7 @@
use Drupal\views\FieldAPIHandlerTrait;
use Drupal\views\Entity\Render\EntityFieldRenderer;
use Drupal\views\Plugin\views\display\DisplayPluginBase;
use Drupal\views\Plugin\DependentWithRemovalPluginInterface;
use Drupal\views\ResultRow;
use Drupal\views\ViewExecutable;
use Symfony\Component\DependencyInjection\ContainerInterface;
......@@ -34,7 +35,7 @@
*
* @ViewsField("field")
*/
class EntityField extends FieldPluginBase implements CacheableDependencyInterface, MultiItemsFieldHandlerInterface {
class EntityField extends FieldPluginBase implements CacheableDependencyInterface, MultiItemsFieldHandlerInterface, DependentWithRemovalPluginInterface {
use FieldAPIHandlerTrait;
use PluginDependencyTrait;
......@@ -1077,4 +1078,29 @@ public function getValue(ResultRow $values, $field = NULL) {
}
}
/**
* {@inheritdoc}
*/
public function onDependencyRemoval(array $dependencies) {
// See if this handler is responsible for any of the dependencies being
// removed. If this is the case, indicate that this handler needs to be
// removed from the View.
$remove = FALSE;
// Get all the current dependencies for this handler.
$current_dependencies = $this->calculateDependencies();
foreach ($current_dependencies as $group => $dependency_list) {
// Check if any of the handler dependencies match the dependencies being
// removed.
foreach ($dependency_list as $config_key) {
if (isset($dependencies[$group]) && array_key_exists($config_key, $dependencies[$group])) {
// This handlers dependency matches a dependency being removed,
// indicate that this handler needs to be removed.
$remove = TRUE;
break 2;
}
}
}
return $remove;
}
}
......@@ -5,6 +5,7 @@
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\image\Entity\ImageStyle;
use Drupal\user\Entity\Role;
use Drupal\views\Entity\View;
/**
......@@ -17,13 +18,23 @@ class ViewsConfigDependenciesIntegrationTest extends ViewsKernelTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['field', 'file', 'image', 'entity_test'];
public static $modules = ['field', 'file', 'image', 'entity_test', 'user', 'text'];
/**
* {@inheritdoc}
*/
public static $testViews = ['entity_test_fields'];
/**
* {@inheritdoc}
*/
protected function setUp($import_test_views = TRUE) {
parent::setUp($import_test_views);
$this->installEntitySchema('user');
$this->installSchema('user', ['users_data']);
}
/**
* Tests integration with image module.
*/
......@@ -69,7 +80,81 @@ public function testImage() {
// Delete the 'foo' image style.
$style->delete();
$view = View::load('entity_test_fields');
// Checks that the view has not been deleted too.
$this->assertNotNull(View::load('entity_test_fields'));
// Checks that the image field was removed from the View.
$display = $view->getDisplay('default');
$this->assertFalse(isset($display['display_options']['fields']['bar']));
// Checks that the view has been disabled.
$this->assertFalse($view->status());
$dependencies = $view->getDependencies() + ['config' => []];
// Checks that the dependency on style 'foo' has been removed.
$this->assertFalse(in_array('image.style.foo', $dependencies['config']));
}
/**
* Tests removing a config dependency that deletes the View.
*/
public function testConfigRemovalRole() {
// Create a role we can add to the View and delete.
$role = Role::create([
'id' => 'dummy',
'label' => 'dummy',
]);
$role->save();
/** @var \Drupal\views\ViewEntityInterface $view */
$view = View::load('entity_test_fields');
$display = &$view->getDisplay('default');
// Set the access to be restricted by the dummy role.
$display['display_options']['access'] = [
'type' => 'role',
'options' => [
'role' => [
$role->id() => $role->id(),
],
],
];
$view->save();
// Check that the View now has a dependency on the Role.
$dependencies = $view->getDependencies() + ['config' => []];
$this->assertTrue(in_array('user.role.dummy', $dependencies['config']));
// Delete the role.
$role->delete();
$view = View::load('entity_test_fields');
// Checks that the view has been deleted too.
$this->assertNull($view);
}
/**
* Tests uninstalling a module that provides a base table for a View.
*/
public function testConfigRemovalBaseTable() {
// Find all the entity types provided by the entity_test module and install
// the schema for them so we can uninstall them.
$entities = \Drupal::entityTypeManager()->getDefinitions();
foreach ($entities as $entity_type_id => $definition) {
if ($definition->getProvider() == 'entity_test') {
$this->installEntitySchema($entity_type_id);
};
}
// Check that removing the module that provides the base table for a View,
// deletes the View.
$this->assertNotNull(View::load('entity_test_fields'));
$this->container->get('module_installer')->uninstall(['entity_test']);
// Check that the View has been deleted.
$this->assertNull(View::load('entity_test_fields'));
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment