From 9b7d24a6fad753a042ebf2a9932a474c65b7ee72 Mon Sep 17 00:00:00 2001 From: Alex Pott Date: Wed, 23 Oct 2024 17:58:12 +0100 Subject: [PATCH] Issue #3481695 by phenaproxima, a.dmitriiev, thejimbirch, b_sharpe, alexpott: Entity displays cloning requires special config action (cherry picked from commit a1b47eafdb98d0dda6208e698f42bd61f64e9e40) --- .../Plugin/ConfigAction/EntityMethod.php | 8 ++-- .../Drupal/Core/Entity/EntityDisplayBase.php | 17 +++++++++ ...BuilderEntityViewDisplayValidationTest.php | 1 + .../Core/Entity/EntityDisplayBaseTest.php | 19 ++++++++++ .../EntityFormDisplayValidationTest.php | 1 + .../EntityViewDisplayValidationTest.php | 1 + .../Recipe/EntityCloneConfigActionTest.php | 37 +++++++++++++++++++ .../Config/Entity/EntityDisplayBaseTest.php | 11 ++++++ 8 files changed, 92 insertions(+), 3 deletions(-) diff --git a/core/lib/Drupal/Core/Config/Action/Plugin/ConfigAction/EntityMethod.php b/core/lib/Drupal/Core/Config/Action/Plugin/ConfigAction/EntityMethod.php index 8b00b4381c1..82ffdf2fab5 100644 --- a/core/lib/Drupal/Core/Config/Action/Plugin/ConfigAction/EntityMethod.php +++ b/core/lib/Drupal/Core/Config/Action/Plugin/ConfigAction/EntityMethod.php @@ -138,12 +138,14 @@ final class EntityMethod implements ConfigActionPluginInterface, ContainerFactor if ($this->numberOfRequiredParams !== 1 && $this->numberOfParams !== 1) { throw new EntityMethodException(sprintf('Entity method config action \'%s\' requires an array value. The number of parameters or required parameters for %s::%s() is not 1', $this->pluginId, $entity->getEntityType()->getClass(), $this->method)); } - $entity->{$this->method}($value); + $result = $entity->{$this->method}($value); } else { - $entity->{$this->method}(...$value); + $result = $entity->{$this->method}(...$value); } - return $entity; + // If an instance of the entity (either itself, or a clone) was returned + // by the method, return that. + return is_a($result, get_class($entity)) ? $result : $entity; } } diff --git a/core/lib/Drupal/Core/Entity/EntityDisplayBase.php b/core/lib/Drupal/Core/Entity/EntityDisplayBase.php index c1b80ac8487..40e713b9d43 100644 --- a/core/lib/Drupal/Core/Entity/EntityDisplayBase.php +++ b/core/lib/Drupal/Core/Entity/EntityDisplayBase.php @@ -324,6 +324,7 @@ abstract class EntityDisplayBase extends ConfigEntityBase implements EntityDispl /** * {@inheritdoc} */ + #[ActionMethod(adminLabel: new TranslatableMarkup('Copy to another mode'), pluralize: FALSE)] public function createCopy($mode) { $display = $this->createDuplicate(); $display->mode = $display->originalMode = $mode; @@ -588,4 +589,20 @@ abstract class EntityDisplayBase extends ConfigEntityBase implements EntityDispl return \Drupal::logger('system'); } + /** + * {@inheritdoc} + */ + public function set($property_name, $value): static { + // If changing the entity ID, also update the target entity type, bundle, + // and view mode. + if ($this->isNew() && $property_name === $this->getEntityType()->getKey('id')) { + if (substr_count($value, '.') !== 2) { + throw new \InvalidArgumentException("'$value' is not a valid entity display ID."); + } + [$this->targetEntityType, $this->bundle, $this->mode] = explode('.', $value); + } + parent::set($property_name, $value); + return $this; + } + } diff --git a/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderEntityViewDisplayValidationTest.php b/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderEntityViewDisplayValidationTest.php index 132b5a8a03e..24eb61c309f 100644 --- a/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderEntityViewDisplayValidationTest.php +++ b/core/modules/layout_builder/tests/src/Kernel/LayoutBuilderEntityViewDisplayValidationTest.php @@ -72,6 +72,7 @@ class LayoutBuilderEntityViewDisplayValidationTest extends ConfigEntityValidatio */ public function testImmutableProperties(array $valid_values = []): void { parent::testImmutableProperties([ + 'id' => 'entity_test_with_bundle.two.full', 'targetEntityType' => 'entity_test_with_bundle', 'bundle' => 'two', ]); diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityDisplayBaseTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityDisplayBaseTest.php index 7514533966c..7720b711c1f 100644 --- a/core/tests/Drupal/KernelTests/Core/Entity/EntityDisplayBaseTest.php +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityDisplayBaseTest.php @@ -6,6 +6,7 @@ namespace Drupal\KernelTests\Core\Entity; use Drupal\comment\Entity\CommentType; use Drupal\Core\Entity\Entity\EntityViewDisplay; +use Drupal\Core\Entity\EntityDisplayRepositoryInterface; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; use Drupal\KernelTests\KernelTestBase; @@ -170,4 +171,22 @@ class EntityDisplayBaseTest extends KernelTestBase { $this->assertSame($expected_dependencies, $entity_display->getDependencies()); } + /** + * Tests that changing the entity ID updates related properties. + */ + public function testChangeId(): void { + /** @var \Drupal\Core\Entity\Display\EntityDisplayInterface $display */ + $display = $this->container->get(EntityDisplayRepositoryInterface::class) + ->getViewDisplay('entity_test', 'entity_test'); + $this->assertSame('entity_test.entity_test.default', $display->id()); + $display->set('id', 'node.page.rss'); + $this->assertSame('node', $display->getTargetEntityTypeId()); + $this->assertSame('page', $display->getTargetBundle()); + $this->assertSame('rss', $display->getMode()); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage("'a.b' is not a valid entity display ID."); + $display->set('id', 'a.b'); + } + } diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityFormDisplayValidationTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityFormDisplayValidationTest.php index ccd069f914d..57762e942bb 100644 --- a/core/tests/Drupal/KernelTests/Core/Entity/EntityFormDisplayValidationTest.php +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityFormDisplayValidationTest.php @@ -113,6 +113,7 @@ class EntityFormDisplayValidationTest extends ConfigEntityValidationTestBase { */ public function testImmutableProperties(array $valid_values = []): void { parent::testImmutableProperties([ + 'id' => 'entity_test_with_bundle.two.default', 'targetEntityType' => 'entity_test_with_bundle', 'bundle' => 'two', ]); diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityViewDisplayValidationTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityViewDisplayValidationTest.php index 607458810cc..7e034801a89 100644 --- a/core/tests/Drupal/KernelTests/Core/Entity/EntityViewDisplayValidationTest.php +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityViewDisplayValidationTest.php @@ -92,6 +92,7 @@ class EntityViewDisplayValidationTest extends ConfigEntityValidationTestBase { */ public function testImmutableProperties(array $valid_values = []): void { parent::testImmutableProperties([ + 'id' => 'entity_test_with_bundle.two.full', 'targetEntityType' => 'entity_test_with_bundle', 'bundle' => 'two', ]); diff --git a/core/tests/Drupal/KernelTests/Core/Recipe/EntityCloneConfigActionTest.php b/core/tests/Drupal/KernelTests/Core/Recipe/EntityCloneConfigActionTest.php index dd9472d81f7..c8ce6a3aa8b 100644 --- a/core/tests/Drupal/KernelTests/Core/Recipe/EntityCloneConfigActionTest.php +++ b/core/tests/Drupal/KernelTests/Core/Recipe/EntityCloneConfigActionTest.php @@ -5,7 +5,10 @@ declare(strict_types=1); namespace Drupal\KernelTests\Core\Recipe; use Drupal\Core\Config\Action\ConfigActionException; +use Drupal\Core\Entity\EntityDisplayRepositoryInterface; +use Drupal\Core\Extension\ModuleInstallerInterface; use Drupal\KernelTests\KernelTestBase; +use Drupal\Tests\node\Traits\ContentTypeCreationTrait; use Drupal\Tests\user\Traits\UserCreationTrait; use Drupal\user\Entity\Role; @@ -15,6 +18,7 @@ use Drupal\user\Entity\Role; */ class EntityCloneConfigActionTest extends KernelTestBase { + use ContentTypeCreationTrait; use UserCreationTrait; /** @@ -69,4 +73,37 @@ class EntityCloneConfigActionTest extends KernelTestBase { $this->assertFalse($clone->hasPermission('access user profiles')); } + /** + * Tests cloning entity displays, which have specialized logic for that. + */ + public function testCloneEntityDisplay(): void { + $this->container->get(ModuleInstallerInterface::class)->install(['node']); + $this->createContentType(['type' => 'alpha']); + $this->createContentType(['type' => 'beta']); + + /** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */ + $display_repository = $this->container->get(EntityDisplayRepositoryInterface::class); + // Create the default view displays for each node type. + $display_repository->getViewDisplay('node', 'alpha')->save(); + $display_repository->getViewDisplay('node', 'beta')->save(); + + // Ensure the `rss` displays don't exist yet. + $this->assertTrue($display_repository->getViewDisplay('node', 'alpha', 'rss')->isNew()); + $this->assertTrue($display_repository->getViewDisplay('node', 'beta', 'rss')->isNew()); + // Use the action to clone the default view displays to the `rss` view mode. + /** @var \Drupal\Core\Config\Action\ConfigActionManager $manager */ + $manager = $this->container->get('plugin.manager.config_action'); + $manager->applyAction('cloneAs', 'core.entity_view_display.node.alpha.default', 'node.alpha.rss'); + $manager->applyAction('entity_method:core.entity_view_display:createCopy', 'core.entity_view_display.node.beta.default', 'rss'); + $this->assertFalse($display_repository->getViewDisplay('node', 'alpha', 'rss')->isNew()); + $this->assertFalse($display_repository->getViewDisplay('node', 'beta', 'rss')->isNew()); + + // Ensure that this also works with wildcards. + $this->assertTrue($display_repository->getViewDisplay('node', 'alpha', 'search_result')->isNew()); + $this->assertTrue($display_repository->getViewDisplay('node', 'beta', 'search_result')->isNew()); + $manager->applyAction('entity_method:core.entity_view_display:createCopy', 'core.entity_view_display.node.*.default', 'search_result'); + $this->assertFalse($display_repository->getViewDisplay('node', 'alpha', 'search_result')->isNew()); + $this->assertFalse($display_repository->getViewDisplay('node', 'beta', 'search_result')->isNew()); + } + } diff --git a/core/tests/Drupal/Tests/Core/Config/Entity/EntityDisplayBaseTest.php b/core/tests/Drupal/Tests/Core/Config/Entity/EntityDisplayBaseTest.php index a45e3d10bd2..35ffd1ecc5c 100644 --- a/core/tests/Drupal/Tests/Core/Config/Entity/EntityDisplayBaseTest.php +++ b/core/tests/Drupal/Tests/Core/Config/Entity/EntityDisplayBaseTest.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Drupal\Tests\Core\Config\Entity; use Drupal\Core\Entity\EntityDisplayBase; +use Drupal\Core\Entity\EntityType; use Drupal\Tests\UnitTestCase; use PHPUnit\Framework\MockObject\MockObject; @@ -25,6 +26,7 @@ class EntityDisplayBaseTest extends UnitTestCase { */ protected function setUp(): void { parent::setUp(); + $this->entityDisplay = $this->getMockBuilder(EntityDisplayBaseMockableClass::class) ->disableOriginalConstructor() ->onlyMethods([]) @@ -91,4 +93,13 @@ class EntityDisplayBaseMockableClass extends EntityDisplayBase { return NULL; } + public function getEntityType() { + return new EntityType([ + 'id' => 'entity_view_display', + 'entity_keys' => [ + 'id' => 'id', + ], + ]); + } + }