diff --git a/core/modules/layout_builder/layout_builder.module b/core/modules/layout_builder/layout_builder.module index abbe052f7e138246444bf39a0d646e2c278a4ac6..d369096505e8be8d9898c24c9e35614ff8740a73 100644 --- a/core/modules/layout_builder/layout_builder.module +++ b/core/modules/layout_builder/layout_builder.module @@ -355,3 +355,15 @@ function layout_builder_quickedit_render_field(EntityInterface $entity, $field_n $quick_edit_integration = \Drupal::classResolver(QuickEditIntegration::class); return $quick_edit_integration->quickEditRenderField($entity, $field_name, $view_mode_id, $langcode); } + +/** + * Implements hook_entity_translation_create(). + */ +function layout_builder_entity_translation_create(EntityInterface $translation) { + /** @var \Drupal\Core\Entity\FieldableEntityInterface $translation */ + if ($translation->hasField(OverridesSectionStorage::FIELD_NAME) && $translation->getFieldDefinition(OverridesSectionStorage::FIELD_NAME)->isTranslatable()) { + // When creating a new translation do not copy untranslated sections because + // per-language layouts are not supported. + $translation->set(OverridesSectionStorage::FIELD_NAME, []); + } +} diff --git a/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php index 9bd77f422cf44f41c47763cbde123d4237b5e2bf..abb65ca7c1c3f6613b754ddff92393197a4d7f5e 100644 --- a/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php +++ b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php @@ -207,6 +207,7 @@ protected function addSectionField($entity_type_id, $bundle, $field_name) { 'type' => 'layout_section', 'locked' => TRUE, ]); + $field_storage->setTranslatable(FALSE); $field_storage->save(); } @@ -215,6 +216,7 @@ protected function addSectionField($entity_type_id, $bundle, $field_name) { 'bundle' => $bundle, 'label' => t('Layout'), ]); + $field->setTranslatable(FALSE); $field->save(); } } diff --git a/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php index 003b3de7f0e76f048b1670ffab438eb5c479d918..b35a9070e42278ba4ba28e4534ece2b8695a5b6d 100644 --- a/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php @@ -397,9 +397,29 @@ public function access($operation, AccountInterface $account = NULL, $return_as_ // Access also depends on the default being enabled. $result = $result->andIf($this->getDefaultSectionStorage()->access($operation, $account, TRUE)); + $result = $this->handleTranslationAccess($result, $operation, $account); return $return_as_object ? $result : $result->isAllowed(); } + /** + * Handles access checks related to translations. + * + * @param \Drupal\Core\Access\AccessResult $result + * The access result. + * @param string $operation + * The operation to be performed. + * @param \Drupal\Core\Session\AccountInterface $account + * The user for which to check access. + * + * @return \Drupal\Core\Access\AccessResultInterface + * The access result. + */ + protected function handleTranslationAccess(AccessResult $result, $operation, AccountInterface $account) { + $entity = $this->getEntity(); + // Access is always denied on non-default translations. + return $result->andIf(AccessResult::allowedIf(!($entity instanceof TranslatableInterface && !$entity->isDefaultTranslation())))->addCacheableDependency($entity); + } + /** * {@inheritdoc} */ diff --git a/core/modules/layout_builder/tests/src/Functional/LayoutBuilderMultilingualTest.php b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderMultilingualTest.php deleted file mode 100644 index b0128418bff1aa4bc12743e5f200155e866306f0..0000000000000000000000000000000000000000 --- a/core/modules/layout_builder/tests/src/Functional/LayoutBuilderMultilingualTest.php +++ /dev/null @@ -1,110 +0,0 @@ -<?php - -namespace Drupal\Tests\layout_builder\Functional; - -use Drupal\block_content\Entity\BlockContentType; -use Drupal\language\Entity\ConfigurableLanguage; -use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay; -use Drupal\Tests\BrowserTestBase; - -/** - * Tests Layout Builder functionality with multiple languages installed. - * - * @group layout_builder - */ -class LayoutBuilderMultilingualTest extends BrowserTestBase { - - /** - * {@inheritdoc} - */ - protected static $modules = [ - 'layout_builder', - 'node', - 'block_content', - 'content_translation', - 'locale', - ]; - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - - // There must be more than one block type available to trigger - // \Drupal\layout_builder\Controller\ChooseBlockController::inlineBlockList(). - BlockContentType::create([ - 'id' => 'first_type', - 'label' => 'First type', - ])->save(); - BlockContentType::create([ - 'id' => 'second_type', - 'label' => 'Second type', - ])->save(); - - // Create a translatable content type with layout overrides enabled. - $this->createContentType([ - 'type' => 'bundle_with_section_field', - ]); - $this->container->get('content_translation.manager')->setEnabled('node', 'bundle_with_section_field', TRUE); - LayoutBuilderEntityViewDisplay::load('node.bundle_with_section_field.default') - ->enableLayoutBuilder() - ->setOverridable() - ->save(); - - // Create a second language. - ConfigurableLanguage::createFromLangcode('es')->save(); - - // Create a node and translate it. - $node = $this->createNode([ - 'type' => 'bundle_with_section_field', - 'title' => 'The untranslated node title', - ]); - $node->addTranslation('es', [ - 'title' => 'The translated node title', - ]); - $node->save(); - - $this->drupalLogin($this->createUser([ - 'configure any layout', - 'translate interface', - 'create and edit custom blocks', - ])); - } - - /** - * Tests that custom blocks are available for translated entities. - */ - public function testCustomBlocks() { - // Check translated and untranslated entities before translating the string. - $this->assertCustomBlocks('node/1/layout'); - $this->assertCustomBlocks('es/node/1/layout'); - - // Translate the 'Inline blocks' string used as a category in - // \Drupal\layout_builder\Controller\ChooseBlockController::inlineBlockList(). - $this->drupalPostForm('admin/config/regional/translate', ['string' => 'Inline blocks'], 'Filter'); - $this->drupalPostForm(NULL, ['Translated string (Spanish)' => 'Bloques en linea'], 'Save translations'); - - // Check translated and untranslated entities after translating the string. - $this->assertCustomBlocks('node/1/layout'); - $this->assertCustomBlocks('es/node/1/layout'); - } - - /** - * Asserts that custom blocks are available. - * - * @param string $url - * The URL for a Layout Builder enabled entity. - */ - protected function assertCustomBlocks($url) { - $page = $this->getSession()->getPage(); - $assert_session = $this->assertSession(); - - $this->drupalGet($url); - $page->clickLink('Add Block'); - $page->clickLink('Create custom block'); - $assert_session->linkExists('First type'); - $assert_session->linkExists('Second type'); - } - -} diff --git a/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTranslationTest.php b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTranslationTest.php new file mode 100644 index 0000000000000000000000000000000000000000..819047cf22e9fd31af20f6ca0b4a04cceb11e056 --- /dev/null +++ b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTranslationTest.php @@ -0,0 +1,218 @@ +<?php + +namespace Drupal\Tests\layout_builder\Functional; + +use Drupal\Tests\content_translation\Functional\ContentTranslationTestBase; +use Drupal\Core\Entity\Entity\EntityViewDisplay; +use Drupal\Core\Url; + +/** + * Tests that the Layout Builder works with translated content. + * + * @group layout_builder + */ +class LayoutBuilderTranslationTest extends ContentTranslationTestBase { + + /** + * {@inheritdoc} + */ + public static $modules = [ + 'content_translation', + 'contextual', + 'entity_test', + 'layout_builder', + 'block', + ]; + + /** + * The entity used for testing. + * + * @var \Drupal\Core\Entity\EntityInterface + */ + protected $entity; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + $this->setUpViewDisplay(); + $this->setUpEntities(); + } + + /** + * Tests that layout overrides work when created after a translation. + */ + public function testTranslationBeforeLayoutOverride() { + $assert_session = $this->assertSession(); + + $this->addEntityTranslation(); + + $entity_url = $this->entity->toUrl()->toString(); + $language = \Drupal::languageManager()->getLanguage($this->langcodes[2]); + $translated_entity_url = $this->entity->toUrl('canonical', ['language' => $language])->toString(); + $translated_layout_url = $translated_entity_url . '/layout'; + + $this->drupalGet($entity_url); + $assert_session->pageTextNotContains('The translated field value'); + $assert_session->pageTextContains('The untranslated field value'); + $assert_session->linkExists('Layout'); + + $this->drupalGet($translated_entity_url); + $assert_session->pageTextNotContains('The untranslated field value'); + $assert_session->pageTextContains('The translated field value'); + $assert_session->linkNotExists('Layout'); + + $this->drupalGet($translated_layout_url); + $assert_session->pageTextContains('Access denied'); + + $this->addLayoutOverride(); + + $this->drupalGet($entity_url); + $assert_session->pageTextNotContains('The translated field value'); + $assert_session->pageTextContains('The untranslated field value'); + $assert_session->pageTextContains('Powered by Drupal'); + + // Ensure that the layout change propagates to the translated entity. + $this->drupalGet($translated_entity_url); + $assert_session->pageTextNotContains('The untranslated field value'); + $assert_session->pageTextContains('The translated field value'); + $assert_session->pageTextContains('Powered by Drupal'); + } + + /** + * Tests that layout overrides work when created before a translation. + */ + public function testLayoutOverrideBeforeTranslation() { + $assert_session = $this->assertSession(); + + $entity_url = $this->entity->toUrl()->toString(); + $language = \Drupal::languageManager()->getLanguage($this->langcodes[2]); + + $this->addLayoutOverride(); + + $this->drupalGet($entity_url); + $assert_session->pageTextNotContains('The translated field value'); + $assert_session->pageTextContains('The untranslated field value'); + $assert_session->pageTextContains('Powered by Drupal'); + $assert_session->linkExists('Layout'); + + $this->addEntityTranslation(); + $translated_entity_url = $this->entity->toUrl('canonical', ['language' => $language])->toString(); + $translated_layout_url = $translated_entity_url . '/layout'; + + $this->drupalGet($entity_url); + $assert_session->pageTextNotContains('The translated field value'); + $assert_session->pageTextContains('The untranslated field value'); + $assert_session->pageTextContains('Powered by Drupal'); + $assert_session->linkExists('Layout'); + + $this->drupalGet($translated_entity_url); + $assert_session->pageTextNotContains('The untranslated field value'); + $assert_session->pageTextContains('The translated field value'); + $assert_session->pageTextContains('Powered by Drupal'); + $assert_session->linkNotExists('Layout'); + + $this->drupalGet($translated_layout_url); + $assert_session->pageTextContains('Access denied'); + } + + /** + * {@inheritdoc} + */ + protected function getAdministratorPermissions() { + $permissions = parent::getAdministratorPermissions(); + $permissions[] = 'administer entity_test_mul display'; + return $permissions; + } + + /** + * {@inheritdoc} + */ + protected function getTranslatorPermissions() { + $permissions = parent::getTranslatorPermissions(); + $permissions[] = 'view test entity translations'; + $permissions[] = 'view test entity'; + $permissions[] = 'configure any layout'; + return $permissions; + } + + /** + * Setup translated entity with layouts. + */ + protected function setUpEntities() { + $this->drupalLogin($this->administrator); + + $field_ui_prefix = 'entity_test_mul/structure/entity_test_mul'; + // Allow overrides for the layout. + $this->drupalPostForm("$field_ui_prefix/display/default", ['layout[enabled]' => TRUE], 'Save'); + $this->drupalPostForm("$field_ui_prefix/display/default", ['layout[allow_custom]' => TRUE], 'Save'); + + // @todo The Layout Builder UI relies on local tasks; fix in + // https://www.drupal.org/project/drupal/issues/2917777. + $this->drupalPlaceBlock('local_tasks_block'); + + // Create a test entity. + $id = $this->createEntity([ + $this->fieldName => [['value' => 'The untranslated field value']], + ], $this->langcodes[0]); + $storage = $this->container->get('entity_type.manager') + ->getStorage($this->entityTypeId); + $storage->resetCache([$id]); + $this->entity = $storage->load($id); + } + + /** + * Set up the View Display. + */ + protected function setUpViewDisplay() { + EntityViewDisplay::create([ + 'targetEntityType' => $this->entityTypeId, + 'bundle' => $this->bundle, + 'mode' => 'default', + 'status' => TRUE, + ])->setComponent($this->fieldName, ['type' => 'string'])->save(); + } + + /** + * Adds an entity translation. + */ + protected function addEntityTranslation() { + $user = $this->loggedInUser; + $this->drupalLogin($this->translator); + $add_translation_url = Url::fromRoute("entity.$this->entityTypeId.content_translation_add", [ + $this->entityTypeId => $this->entity->id(), + 'source' => $this->langcodes[0], + 'target' => $this->langcodes[2], + ]); + $this->drupalPostForm($add_translation_url, [ + "{$this->fieldName}[0][value]" => 'The translated field value', + ], 'Save'); + $this->drupalLogin($user); + } + + /** + * Adds a layout override. + */ + protected function addLayoutOverride() { + $assert_session = $this->assertSession(); + $page = $this->getSession()->getPage(); + $entity_url = $this->entity->toUrl()->toString(); + $layout_url = $entity_url . '/layout'; + $this->drupalGet($layout_url); + $assert_session->pageTextNotContains('The translated field value'); + $assert_session->pageTextContains('The untranslated field value'); + + // Adjust the layout. + $assert_session->linkExists('Add Block'); + $this->clickLink('Add Block'); + $assert_session->linkExists('Powered by Drupal'); + $this->clickLink('Powered by Drupal'); + $page->pressButton('Add Block'); + + $assert_session->pageTextContains('Powered by Drupal'); + $assert_session->buttonExists('Save layout'); + $page->pressButton('Save layout'); + } + +} diff --git a/core/modules/layout_builder/tests/src/Functional/LayoutSectionTest.php b/core/modules/layout_builder/tests/src/Functional/LayoutSectionTest.php index 1041dd813ebb9c518c062459a77c5aaa0495f06d..86ae521b1044fad48264cf7ce787ce289aa7fd93 100644 --- a/core/modules/layout_builder/tests/src/Functional/LayoutSectionTest.php +++ b/core/modules/layout_builder/tests/src/Functional/LayoutSectionTest.php @@ -2,7 +2,6 @@ namespace Drupal\Tests\layout_builder\Functional; -use Drupal\language\Entity\ConfigurableLanguage; use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay; use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage; use Drupal\layout_builder\Section; @@ -199,50 +198,6 @@ public function testLayoutSectionFormatterAccess() { $this->assertLayoutSection('.layout--onecol', 'Hello test world', '', '', 'UNCACHEABLE'); } - /** - * Tests the multilingual support of the section formatter. - */ - public function testMultilingualLayoutSectionFormatter() { - $this->container->get('module_installer')->install(['content_translation']); - $this->rebuildContainer(); - - ConfigurableLanguage::createFromLangcode('es')->save(); - $this->container->get('content_translation.manager')->setEnabled('node', 'bundle_with_section_field', TRUE); - - $entity = $this->createSectionNode([ - [ - 'section' => new Section('layout_onecol', [], [ - 'baz' => new SectionComponent('baz', 'content', [ - 'id' => 'system_powered_by_block', - ]), - ]), - ], - ]); - $entity->addTranslation('es', [ - 'title' => 'Translated node title', - OverridesSectionStorage::FIELD_NAME => [ - [ - 'section' => new Section('layout_twocol', [], [ - 'foo' => new SectionComponent('foo', 'first', [ - 'id' => 'test_block_instantiation', - 'display_message' => 'foo text', - ]), - 'bar' => new SectionComponent('bar', 'second', [ - 'id' => 'test_block_instantiation', - 'display_message' => 'bar text', - ]), - ]), - ], - ], - ]); - $entity->save(); - - $this->drupalGet($entity->toUrl('canonical')); - $this->assertLayoutSection('.layout--onecol', 'Powered by'); - $this->drupalGet($entity->toUrl('canonical')->setOption('prefix', 'es/')); - $this->assertLayoutSection('.layout--twocol', ['foo text', 'bar text']); - } - /** * Ensures that the entity title is displayed. */ diff --git a/core/modules/layout_builder/tests/src/Kernel/OverridesSectionStorageTest.php b/core/modules/layout_builder/tests/src/Kernel/OverridesSectionStorageTest.php index 4a1469254a42f3e6af7d9b6c6873e43292db8797..66b4f1434b41348780ee2e00fbfc907c3ed2bd9d 100644 --- a/core/modules/layout_builder/tests/src/Kernel/OverridesSectionStorageTest.php +++ b/core/modules/layout_builder/tests/src/Kernel/OverridesSectionStorageTest.php @@ -7,6 +7,7 @@ use Drupal\Core\Plugin\Context\EntityContext; use Drupal\entity_test\Entity\EntityTest; use Drupal\KernelTests\KernelTestBase; +use Drupal\language\Entity\ConfigurableLanguage; use Drupal\layout_builder\DefaultsSectionStorageInterface; use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay; use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage; @@ -34,6 +35,7 @@ class OverridesSectionStorageTest extends KernelTestBase { 'field', 'system', 'user', + 'language', ]; /** @@ -97,6 +99,20 @@ public function testAccess($expected, $is_enabled, array $section_data, array $p $this->assertSame($expected, $result); $result = $this->plugin->access('view', $account); $this->assertSame($expected, $result); + + // Create a translation. + ConfigurableLanguage::createFromLangcode('es')->save(); + $entity = EntityTest::load($entity->id()); + $translation = $entity->addTranslation('es'); + $translation->save(); + $this->plugin->setContext('entity', EntityContext::fromEntity($translation)); + + // Perform the same checks again but with a non default translation which + // should always deny access. + $result = $this->plugin->access('view'); + $this->assertSame(FALSE, $result); + $result = $this->plugin->access('view', $account); + $this->assertSame(FALSE, $result); } /** diff --git a/core/modules/layout_builder/tests/src/Kernel/TranslatableFieldTest.php b/core/modules/layout_builder/tests/src/Kernel/TranslatableFieldTest.php new file mode 100644 index 0000000000000000000000000000000000000000..d22d7ea287774c4023d4450fe1149233f7b1c316 --- /dev/null +++ b/core/modules/layout_builder/tests/src/Kernel/TranslatableFieldTest.php @@ -0,0 +1,86 @@ +<?php + +namespace Drupal\Tests\layout_builder\Kernel; + +use Drupal\entity_test\Entity\EntityTest; +use Drupal\field\Entity\FieldConfig; +use Drupal\field\Entity\FieldStorageConfig; +use Drupal\KernelTests\KernelTestBase; +use Drupal\language\Entity\ConfigurableLanguage; +use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay; +use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage; +use Drupal\layout_builder\Section; +use Drupal\layout_builder\SectionComponent; + +/** + * Tests Layout Builder with a translatable layout field. + * + * @group layout_builder + */ +class TranslatableFieldTest extends KernelTestBase { + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'layout_discovery', + 'layout_builder', + 'entity_test', + 'field', + 'system', + 'user', + 'language', + ]; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->installSchema('system', ['key_value_expire']); + $this->installEntitySchema('entity_test'); + + // Create a translation. + ConfigurableLanguage::createFromLangcode('es')->save(); + + LayoutBuilderEntityViewDisplay::create([ + 'targetEntityType' => 'entity_test', + 'bundle' => 'entity_test', + 'mode' => 'default', + 'status' => TRUE, + ]) + ->enableLayoutBuilder() + ->setOverridable() + ->save(); + + FieldStorageConfig::loadByName('entity_test', OverridesSectionStorage::FIELD_NAME) + ->setTranslatable(TRUE) + ->save(); + FieldConfig::loadByName('entity_test', 'entity_test', OverridesSectionStorage::FIELD_NAME) + ->setTranslatable(TRUE) + ->save(); + } + + /** + * Tests that sections on cleared when creating a new translation. + */ + public function testSectionsClearedOnCreateTranslation() { + $section_data = [ + new Section('layout_default', [], [ + 'first-uuid' => new SectionComponent('first-uuid', 'content', ['id' => 'foo']), + ]), + ]; + $entity = EntityTest::create([OverridesSectionStorage::FIELD_NAME => $section_data]); + $entity->save(); + $this->assertFalse($entity->get(OverridesSectionStorage::FIELD_NAME)->isEmpty()); + + $entity = EntityTest::load($entity->id()); + /** @var \Drupal\entity_test\Entity\EntityTest $translation */ + $translation = $entity->addTranslation('es', $entity->toArray()); + + // Per-language layouts are not supported. + $this->assertTrue($translation->get(OverridesSectionStorage::FIELD_NAME)->isEmpty()); + } + +}