diff --git a/config/schema/entity.schema.yml b/config/schema/entity.schema.yml index a70ef2a1fb704a377884034785bf44140b9a44d2..b3c19235842c0e15f1caa27f55071087a85bb5e9 100644 --- a/config/schema/entity.schema.yml +++ b/config/schema/entity.schema.yml @@ -1,3 +1,7 @@ +action.configuration.entity_delete_action:*: + type: action_configuration_default + label: 'Delete entity configuration' + views.field.rendered_entity: type: views_field label: 'Rendered entity' diff --git a/src/Form/DeleteMultiple.php b/src/Form/DeleteMultiple.php new file mode 100644 index 0000000000000000000000000000000000000000..7fe04ff5e95ae23c9f7df5223e0bfc1eda31e60c --- /dev/null +++ b/src/Form/DeleteMultiple.php @@ -0,0 +1,231 @@ +<?php + +/** + * @file + * Contains \Drupal\entity\Form\DeleteMultiple. + */ + +namespace Drupal\entity\Form; + +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Form\ConfirmFormBase; +use Drupal\Core\Url; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Session\AccountInterface; +use Drupal\user\PrivateTempStoreFactory; +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Provides an entities deletion confirmation form. + */ +class DeleteMultiple extends ConfirmFormBase { + + /** + * The current user. + * + * @var \Drupal\Core\Session\AccountInterface + */ + protected $currentUser; + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * The tempstore. + * + * @var \Drupal\user\SharedTempStore + */ + protected $tempStore; + + /** + * The entity type id. + * + * @var string + */ + protected $entityTypeId; + + /** + * The selection, in the entity_id => langcodes format. + * + * @var array + */ + protected $selection = []; + + /** + * Constructs a new DeleteMultiple object. + * + * @param \Drupal\Core\Session\AccountInterface $current_user + * The current user. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + * @param \Drupal\user\PrivateTempStoreFactory $temp_store_factory + * The tempstore factory. + */ + public function __construct(AccountInterface $current_user, EntityTypeManagerInterface $entity_type_manager, PrivateTempStoreFactory $temp_store_factory) { + $this->currentUser = $current_user; + $this->entityTypeManager = $entity_type_manager; + $this->tempStore = $temp_store_factory->get('entity_delete_multiple_confirm'); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('current_user'), + $container->get('entity_type.manager'), + $container->get('user.private_tempstore') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'entity_delete_multiple_confirm'; + } + + /** + * {@inheritdoc} + */ + public function getQuestion() { + return $this->formatPlural(count($this->selection), 'Are you sure you want to delete this item?', 'Are you sure you want to delete these items?'); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + return new Url('entity.' . $this->entityTypeId . '.collection'); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return $this->t('Delete'); + } + + /** + * {@inheritdoc} + * + * @param string $entity_type_id + * The entity type id. + */ + public function buildForm(array $form, FormStateInterface $form_state, $entity_type_id = NULL) { + $this->entityTypeId = $entity_type_id; + $this->selection = $this->tempStore->get($this->currentUser->id()); + if (empty($this->entityTypeId) || empty($this->selection)) { + return new RedirectResponse($this->getCancelUrl()->setAbsolute()->toString()); + } + + $storage = $this->entityTypeManager->getStorage($this->entityTypeId); + /** @var \Drupal\Core\Entity\ContentEntityInterface[] $entities */ + $entities = $storage->loadMultiple(array_keys($this->selection)); + $items = []; + foreach ($this->selection as $id => $langcodes) { + foreach ($langcodes as $langcode) { + $entity = $entities[$id]->getTranslation($langcode); + $key = $id . ':' . $langcode; + $default_key = $id . ':' . $entity->getUntranslated()->language()->getId(); + + // If we have a translated entity we build a nested list of translations + // that will be deleted. + $languages = $entity->getTranslationLanguages(); + if (count($languages) > 1 && $entity->isDefaultTranslation()) { + $names = []; + foreach ($languages as $translation_langcode => $language) { + $names[] = $language->getName(); + unset($items[$id . ':' . $translation_langcode]); + } + $items[$default_key] = [ + 'label' => [ + '#markup' => $this->t('@label (Original translation) - <em>The following translations will be deleted:</em>', ['@label' => $entity->label()]), + ], + 'deleted_translations' => [ + '#theme' => 'item_list', + '#items' => $names, + ], + ]; + } + elseif (!isset($items[$default_key])) { + $items[$key] = $entity->label(); + } + } + } + + $form['entities'] = [ + '#theme' => 'item_list', + '#items' => $items, + ]; + $form = parent::buildForm($form, $form_state); + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $total_count = 0; + $delete_entities = []; + $delete_translations = []; + $storage = $this->entityTypeManager->getStorage($this->entityTypeId); + /** @var \Drupal\Core\Entity\ContentEntityInterface[] $entities */ + $entities = $storage->loadMultiple(array_keys($this->selection)); + + foreach ($this->selection as $id => $langcodes) { + foreach ($langcodes as $langcode) { + $entity = $entities[$id]->getTranslation($langcode); + if ($entity->isDefaultTranslation()) { + $delete_entities[$id] = $entity; + unset($delete_translations[$id]); + $total_count += count($entity->getTranslationLanguages()); + } + elseif (!isset($delete_entities[$id])) { + $delete_translations[$id][] = $entity; + } + } + } + + if ($delete_entities) { + $storage->delete($delete_entities); + $this->logger('content')->notice('Deleted @count @entity_type items.', [ + '@count' => count($delete_entities), + '@entity_type' => $this->entityTypeId, + ]); + } + + if ($delete_translations) { + $count = 0; + /** @var \Drupal\Core\Entity\ContentEntityInterface[][] $delete_translations */ + foreach ($delete_translations as $id => $translations) { + $entity = $entities[$id]->getUntranslated(); + foreach ($translations as $translation) { + $entity->removeTranslation($translation->language()->getId()); + } + $entity->save(); + $count += count($translations); + } + if ($count) { + $total_count += $count; + $this->logger('content')->notice('Deleted @count @entity_type translations.', [ + '@count' => $count, + '@entity_type' => $this->entityTypeId, + ]); + } + } + + if ($total_count) { + drupal_set_message($this->formatPlural($total_count, 'Deleted 1 item.', 'Deleted @count items.')); + } + $this->tempStore->delete($this->currentUser->id()); + $form_state->setRedirectUrl($this->getCancelUrl()); + } + +} diff --git a/src/Plugin/Action/DeleteAction.php b/src/Plugin/Action/DeleteAction.php new file mode 100644 index 0000000000000000000000000000000000000000..8812fd56d9d7c60aa24f5c40cf6a0b0499fb58e1 --- /dev/null +++ b/src/Plugin/Action/DeleteAction.php @@ -0,0 +1,102 @@ +<?php + +/** + * @file + * Contains \Drupal\entity\Plugin\Action\DeleteAction. + */ + +namespace Drupal\entity\Plugin\Action; + +use Drupal\Core\Action\ActionBase; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\Session\AccountInterface; +use Drupal\user\PrivateTempStoreFactory; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Redirects to an entity deletion form. + * + * @Action( + * id = "entity_delete_action", + * label = @Translation("Delete entity"), + * deriver = "Drupal\entity\Plugin\Action\Derivative\DeleteActionDeriver", + * ) + */ +class DeleteAction extends ActionBase implements ContainerFactoryPluginInterface { + + /** + * The tempstore object. + * + * @var \Drupal\user\SharedTempStore + */ + protected $tempStore; + + /** + * The current user. + * + * @var \Drupal\Core\Session\AccountInterface + */ + protected $currentUser; + + /** + * Constructs a new DeleteAction object. + * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * The plugin ID for the plugin instance. + * @param mixed $plugin_definition + * The plugin implementation definition. + * @param \Drupal\user\PrivateTempStoreFactory $temp_store_factory + * The tempstore factory. + * @param AccountInterface $current_user + * Current user. + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, PrivateTempStoreFactory $temp_store_factory, AccountInterface $current_user) { + $this->currentUser = $current_user; + $this->tempStore = $temp_store_factory->get('entity_delete_multiple_confirm'); + + parent::__construct($configuration, $plugin_id, $plugin_definition); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('user.private_tempstore'), + $container->get('current_user') + ); + } + + /** + * {@inheritdoc} + */ + public function executeMultiple(array $entities) { + /** @var \Drupal\Core\Entity\ContentEntityInterface[] $entities */ + $selection = []; + foreach ($entities as $entity) { + $langcode = $entity->language()->getId(); + $selection[$entity->id()][$langcode] = $langcode; + } + $this->tempStore->set($this->currentUser->id(), $selection); + } + + /** + * {@inheritdoc} + */ + public function execute($object = NULL) { + $this->executeMultiple([$object]); + } + + /** + * {@inheritdoc} + */ + public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) { + return $object->access('delete', $account, $return_as_object); + } + +} diff --git a/src/Plugin/Action/Derivative/DeleteActionDeriver.php b/src/Plugin/Action/Derivative/DeleteActionDeriver.php new file mode 100644 index 0000000000000000000000000000000000000000..3c86bcc7c0edb4aafbe854ad4b70d16501f4bb78 --- /dev/null +++ b/src/Plugin/Action/Derivative/DeleteActionDeriver.php @@ -0,0 +1,83 @@ +<?php + +/** + * @file + * Contains \Drupal\entity\Plugin\Action\Derivative\DeleteActionDeriver. + */ + +namespace Drupal\entity\Plugin\Action\Derivative; + +use Drupal\Component\Plugin\Derivative\DeriverBase; +use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface; +use Drupal\Core\Entity\ContentEntityInterface; +use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Provides a delete action for each content entity type. + */ +class DeleteActionDeriver extends DeriverBase implements ContainerDeriverInterface { + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityManagerInterface + */ + protected $entityTypeManager; + + /** + * Constructs a new DeleteActionDeriver object. + * + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + */ + public function __construct(EntityTypeManagerInterface $entity_type_manager) { + $this->entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, $base_plugin_id) { + return new static($container->get('entity_type.manager')); + } + + /** + * {@inheritdoc} + */ + public function getDerivativeDefinitions($base_plugin_definition) { + if (empty($this->derivatives)) { + $definitions = []; + foreach ($this->getParticipatingEntityTypes() as $entity_type_id => $entity_type) { + $definition = $base_plugin_definition; + $definition['label'] = t('Delete @entity_type', ['@entity_type' => $entity_type->getLowercaseLabel()]); + $definition['type'] = $entity_type_id; + $definition['confirm_form_route_name'] = 'entity.' . $entity_type_id . '.delete_multiple_form'; + $definitions[$entity_type_id] = $definition; + } + $this->derivatives = $definitions; + } + + return parent::getDerivativeDefinitions($base_plugin_definition); + } + + /** + * Gets a list of participating entity types. + * + * The list consists of all content entity types with a delete-multiple-form + * link template. + * + * @return \Drupal\Core\Entity\EntityTypeInterface[] + * The participating entity types, keyed by entity type id. + */ + protected function getParticipatingEntityTypes() { + $entity_types = $this->entityTypeManager->getDefinitions(); + $entity_types = array_filter($entity_types, function (EntityTypeInterface $entity_type) { + return $entity_type->isSubclassOf(ContentEntityInterface::class) && $entity_type->hasLinkTemplate('delete-multiple-form'); + }); + + return $entity_types; + } + +} diff --git a/src/Routing/DeleteMultipleRouteProvider.php b/src/Routing/DeleteMultipleRouteProvider.php new file mode 100644 index 0000000000000000000000000000000000000000..edbbc38291eb9863c1a68878445cdb651e884d67 --- /dev/null +++ b/src/Routing/DeleteMultipleRouteProvider.php @@ -0,0 +1,52 @@ +<?php + +/** + * @file + * Contains \Drupal\entity\Routing\DeleteMultipleRouteProvider. + */ + +namespace Drupal\entity\Routing; + +use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Entity\Routing\EntityRouteProviderInterface; +use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouteCollection; + +/** + * Provides the HTML route for deleting multiple entities. + */ +class DeleteMultipleRouteProvider implements EntityRouteProviderInterface { + + /** + * {@inheritdoc} + */ + public function getRoutes(EntityTypeInterface $entity_type) { + $routes = new RouteCollection(); + if ($route = $this->deleteMultipleFormRoute($entity_type)) { + $routes->add('entity.' . $entity_type->id() . '.delete_multiple_form', $route); + } + + return $routes; + } + + /** + * Returns the delete multiple form route. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The entity type. + * + * @return \Symfony\Component\Routing\Route|null + * The generated route, if available. + */ + protected function deleteMultipleFormRoute(EntityTypeInterface $entity_type) { + if ($entity_type->hasLinkTemplate('delete-multiple-form')) { + $route = new Route($entity_type->getLinkTemplate('delete-multiple-form')); + $route->setDefault('_form', '\Drupal\entity\Form\DeleteMultiple'); + $route->setDefault('entity_type_id', $entity_type->id()); + $route->setRequirement('_permission', $entity_type->getAdminPermission()); + + return $route; + } + } + +} diff --git a/tests/modules/entity_module_test/config/schema/entity_module_test.schema.yml b/tests/modules/entity_module_test/config/schema/entity_module_test.schema.yml index d4539912532484122d64f1b9a3e42f256d4fc693..eb1c5a089f08343de668ad896fd005a21a4a3e30 100644 --- a/tests/modules/entity_module_test/config/schema/entity_module_test.schema.yml +++ b/tests/modules/entity_module_test/config/schema/entity_module_test.schema.yml @@ -1,6 +1,6 @@ entity_module_test.entity_test_enhanced_bundle.*: type: config_entity - label: 'Entity test with enhancments - Bundle' + label: 'Entity test with enhancements - Bundle' mapping: id: type: string diff --git a/tests/modules/entity_module_test/entity_module_test.routing.yml b/tests/modules/entity_module_test/entity_module_test.routing.yml new file mode 100644 index 0000000000000000000000000000000000000000..3724a6524dfac2c86ae0f6bebf1199a87259b55e --- /dev/null +++ b/tests/modules/entity_module_test/entity_module_test.routing.yml @@ -0,0 +1,7 @@ +entity.entity_test_enhanced.collection: + path: '/entity_test_enhanced' + defaults: + _entity_list: 'entity_test_enhanced' + _title: 'Entity test with enhancements' + requirements: + _permission: 'administer entity_test_enhanced' diff --git a/tests/modules/entity_module_test/src/Entity/EnhancedEntity.php b/tests/modules/entity_module_test/src/Entity/EnhancedEntity.php index 53c7ae441dc862b1ad97bd6f13c5dd6c4691954e..d199f3d653743818bc5cab93a4194190e1139bf5 100644 --- a/tests/modules/entity_module_test/src/Entity/EnhancedEntity.php +++ b/tests/modules/entity_module_test/src/Entity/EnhancedEntity.php @@ -30,7 +30,9 @@ use Drupal\entity\Revision\RevisionableContentEntityBase; * "html" = "\Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider", * "revision" = "\Drupal\entity\Routing\RevisionRouteProvider", * "create" = "\Drupal\entity\Routing\CreateHtmlRouteProvider", + * "delete-multiple" = "\Drupal\entity\Routing\DeleteMultipleRouteProvider", * }, + * "list_builder" = "\Drupal\Core\Entity\EntityListBuilder", * }, * base_table = "entity_test_enhanced", * data_table = "entity_test_enhanced_field_data", @@ -49,6 +51,8 @@ use Drupal\entity\Revision\RevisionableContentEntityBase; * "add-page" = "/entity_test_enhanced/add", * "add-form" = "/entity_test_enhanced/add/{type}", * "canonical" = "/entity_test_enhanced/{entity_test_enhanced}", + * "collection" = "/entity_test_enhanced", + * "delete-multiple-form" = "/entity_test_enhanced/delete", * "revision" = "/entity_test_enhanced/{entity_test_enhanced}/revisions/{entity_test_enhanced_revision}/view", * "revision-revert-form" = "/entity_test_enhanced/{entity_test_enhanced}/revisions/{entity_test_enhanced_revision}/revert", * "version-history" = "/entity_test_enhanced/{entity_test_enhanced}/revisions", diff --git a/tests/src/Functional/DeleteMultipleFormTest.php b/tests/src/Functional/DeleteMultipleFormTest.php new file mode 100644 index 0000000000000000000000000000000000000000..50fb6cd8796cd45c19c22c18756d2e55ccfe06df --- /dev/null +++ b/tests/src/Functional/DeleteMultipleFormTest.php @@ -0,0 +1,86 @@ +<?php + +/** + * @file + * Contains \Drupal\Tests\entity\Functional\DeleteMultipleFormTest. + */ + +namespace Drupal\Tests\entity\Functional; + +use Drupal\entity_module_test\Entity\EnhancedEntity; +use Drupal\entity_module_test\Entity\EnhancedEntityBundle; +use Drupal\simpletest\BrowserTestBase; + +/** + * Tests the delete multiple confirmation form. + * + * @group entity + * @runTestsInSeparateProcesses + * @preserveGlobalState disabled + */ +class DeleteMultipleFormTest extends BrowserTestBase { + + /** + * The current user. + * + * @var \Drupal\Core\Session\AccountInterface; + */ + protected $account; + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = ['entity_module_test', 'user', 'entity']; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + EnhancedEntityBundle::create([ + 'id' => 'default', + 'label' => 'Default', + ])->save(); + $this->account = $this->drupalCreateUser(['administer entity_test_enhanced']); + $this->drupalLogin($this->account); + } + + /** + * Tests the add page. + */ + public function testForm() { + $entities = []; + $selection = []; + for ($i = 0; $i < 2; $i++) { + $entity = EnhancedEntity::create([ + 'type' => 'default', + ]); + $entity->save(); + $entities[$entity->id()] = $entity; + + $langcode = $entity->language()->getId(); + $selection[$entity->id()][$langcode] = $langcode; + } + // Add the selection to the tempstore just like DeleteAction would. + $tempstore = \Drupal::service('user.private_tempstore')->get('entity_delete_multiple_confirm'); + $tempstore->set($this->account->id(), $selection); + + $this->drupalGet('/entity_test_enhanced/delete'); + $assert = $this->assertSession(); + $assert->statusCodeEquals(200); + $assert->elementTextContains('css', '.page-title', 'Are you sure you want to delete these items?'); + $delete_button = $this->getSession()->getPage()->findButton('Delete'); + $delete_button->click(); + $assert = $this->assertSession(); + $assert->addressEquals('/entity_test_enhanced'); + $assert->responseContains('Deleted 2 items.'); + + \Drupal::entityTypeManager()->getStorage('entity_test_enhanced')->resetCache(); + $remaining_entities = EnhancedEntity::loadMultiple(array_keys($selection)); + $this->assertEmpty($remaining_entities); + } + +} diff --git a/tests/src/Kernel/DeleteActionTest.php b/tests/src/Kernel/DeleteActionTest.php new file mode 100644 index 0000000000000000000000000000000000000000..411ced102e96a94751b69fdbc8c75c1d1c466b36 --- /dev/null +++ b/tests/src/Kernel/DeleteActionTest.php @@ -0,0 +1,87 @@ +<?php + +/** + * @file + * Contains \Drupal\Tests\entity\Kernel\DeleteActionTest. + */ + +namespace Drupal\Tests\entity\Kernel; + +use Drupal\entity\Plugin\Action\DeleteAction; +use Drupal\entity_module_test\Entity\EnhancedEntity; +use Drupal\entity_module_test\Entity\EnhancedEntityBundle; +use Drupal\system\Entity\Action; +use Drupal\user\Entity\User; +use Drupal\KernelTests\KernelTestBase; + +/** + * Tests the delete entity action. + */ +class DeleteActionTest extends KernelTestBase { + + /** + * The current user. + * + * @var \Drupal\user\UserInterface + */ + protected $user; + + /** + * {@inheritdoc} + */ + public static $modules = ['action', 'node', 'entity_module_test', 'entity', + 'user', 'system']; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->installEntitySchema('user'); + $this->installEntitySchema('entity_test_enhanced'); + $this->installSchema('system', ['key_value_expire', 'sequences']); + + $bundle = EnhancedEntityBundle::create([ + 'id' => 'default', + 'label' => 'Default', + ]); + $bundle->save(); + + $this->user = User::create([ + 'name' => 'username', + 'status' => 1, + ]); + $this->user->save(); + \Drupal::service('current_user')->setAccount($this->user); + } + + public function testAction() { + /** @var \Drupal\system\ActionConfigEntityInterface $action */ + $action = Action::create([ + 'id' => 'enhanced_entity_delete_action', + 'label' => 'Delete enhanced entity', + 'plugin' => 'entity_delete_action:entity_test_enhanced', + ]); + $status = $action->save(); + $this->assertEquals(SAVED_NEW, $status); + $this->assertInstanceOf(DeleteAction::class, $action->getPlugin()); + + $entities = []; + for ($i = 0; $i < 2; $i++) { + $entity = EnhancedEntity::create([ + 'type' => 'default', + ]); + $entity->save(); + $entities[$entity->id()] = $entity; + } + + $action->execute($entities); + // Confirm that the entity ids and langcodes are now in the tempstore. + $tempstore = \Drupal::service('user.private_tempstore')->get('entity_delete_multiple_confirm'); + $selection = $tempstore->get($this->user->id()); + $this->assertEquals(array_keys($entities), array_keys($selection)); + $this->assertEquals([['en' => 'en'], ['en' => 'en']], array_values($selection)); + } + +}