diff --git a/core/modules/layout_builder/layout_builder.routing.yml b/core/modules/layout_builder/layout_builder.routing.yml index f034a5a1b2..1cfb25e3fd 100644 --- a/core/modules/layout_builder/layout_builder.routing.yml +++ b/core/modules/layout_builder/layout_builder.routing.yml @@ -6,7 +6,7 @@ layout_builder.choose_section: requirements: _permission: 'configure any layout' _layout_builder_access: 'view' - _layout_builder_translation_access: 'view' + _layout_builder_translation_access: 'untranslated' options: _admin_route: TRUE parameters: @@ -20,7 +20,7 @@ layout_builder.add_section: requirements: _permission: 'configure any layout' _layout_builder_access: 'view' - _layout_builder_translation_access: 'view' + _layout_builder_translation_access: 'untranslated' options: _admin_route: TRUE parameters: @@ -38,7 +38,7 @@ layout_builder.configure_section: requirements: _permission: 'configure any layout' _layout_builder_access: 'view' - _layout_builder_translation_access: 'view' + _layout_builder_translation_access: 'untranslated' options: _admin_route: TRUE parameters: @@ -52,7 +52,7 @@ layout_builder.remove_section: requirements: _permission: 'configure any layout' _layout_builder_access: 'view' - _layout_builder_translation_access: 'view' + _layout_builder_translation_access: 'untranslated' options: _admin_route: TRUE parameters: @@ -67,7 +67,7 @@ layout_builder.choose_block: requirements: _permission: 'configure any layout' _layout_builder_access: 'view' - _layout_builder_translation_access: 'view' + _layout_builder_translation_access: 'untranslated' options: _admin_route: TRUE parameters: @@ -82,7 +82,7 @@ layout_builder.add_block: requirements: _permission: 'configure any layout' _layout_builder_access: 'view' - _layout_builder_translation_access: 'view' + _layout_builder_translation_access: 'untranslated' options: _admin_route: TRUE parameters: @@ -96,7 +96,7 @@ layout_builder.choose_inline_block: _title: 'Add a new Inline Block' requirements: _permission: 'configure any layout' - _layout_builder_translation_access: 'view' + _layout_builder_translation_access: 'untranslated' options: _admin_route: TRUE parameters: @@ -111,7 +111,7 @@ layout_builder.update_block: requirements: _permission: 'configure any layout' _layout_builder_access: 'view' - _layout_builder_translation_access: 'view' + _layout_builder_translation_access: 'untranslated' options: _admin_route: TRUE parameters: @@ -126,6 +126,7 @@ layout_builder.translate_block: requirements: _permission: 'configure any layout' _layout_builder_access: 'view' + _layout_builder_translation_access: 'translated' options: _admin_route: TRUE parameters: @@ -139,7 +140,7 @@ layout_builder.remove_block: requirements: _permission: 'configure any layout' _layout_builder_access: 'view' - _layout_builder_translation_access: 'view' + _layout_builder_translation_access: 'untranslated' options: _admin_route: TRUE parameters: @@ -159,7 +160,7 @@ layout_builder.move_block: requirements: _permission: 'configure any layout' _layout_builder_access: 'view' - _layout_builder_translation_access: 'view' + _layout_builder_translation_access: 'untranslated' options: _admin_route: TRUE parameters: diff --git a/core/modules/layout_builder/src/Access/LayoutBuilderTranslationAccessCheck.php b/core/modules/layout_builder/src/Access/LayoutBuilderTranslationAccessCheck.php index f3df40112a..a4f016a9aa 100644 --- a/core/modules/layout_builder/src/Access/LayoutBuilderTranslationAccessCheck.php +++ b/core/modules/layout_builder/src/Access/LayoutBuilderTranslationAccessCheck.php @@ -7,6 +7,7 @@ use Drupal\Core\Routing\Access\AccessInterface; use Drupal\layout_builder\SectionStorageInterface; use Drupal\layout_builder\TranslatableSectionStorageInterface; +use Symfony\Component\Routing\Route; /** * Provides an access check for the Layout Builder translations. @@ -23,15 +24,24 @@ class LayoutBuilderTranslationAccessCheck implements AccessInterface { * * @param \Drupal\layout_builder\SectionStorageInterface $section_storage * The section storage. + * @param \Symfony\Component\Routing\Route $route + * The route to check against. * * @return \Drupal\Core\Access\AccessResultInterface * The access result. */ - public function access(SectionStorageInterface $section_storage) { - $access = AccessResult::allowedIf(!($section_storage instanceof TranslatableSectionStorageInterface && !$section_storage->isDefaultTranslation())); + public function access(SectionStorageInterface $section_storage, Route $route) { + $translation_type = $route->getRequirement('_layout_builder_translation_access'); + if ($translation_type === 'untranslated') { + $access = AccessResult::allowedIf(!$section_storage instanceof TranslatableSectionStorageInterface || $section_storage->isDefaultTranslation()); + } + elseif ($translation_type === 'translated') { + $access = AccessResult::allowedIf($section_storage instanceof TranslatableSectionStorageInterface && !$section_storage->isDefaultTranslation()); + } if ($access instanceof RefinableCacheableDependencyInterface) { $access->addCacheableDependency($section_storage); } return $access; } + } diff --git a/core/modules/layout_builder/src/Element/LayoutBuilder.php b/core/modules/layout_builder/src/Element/LayoutBuilder.php index 9b05fe9d26..2e3b021f62 100644 --- a/core/modules/layout_builder/src/Element/LayoutBuilder.php +++ b/core/modules/layout_builder/src/Element/LayoutBuilder.php @@ -108,7 +108,7 @@ public function preRender($element) { */ protected function layout(SectionStorageInterface $section_storage) { $this->prepareLayout($section_storage); - $is_translation = $section_storage instanceof TranslatableSectionStorageInterface && !$section_storage->isDefaultTranslation(); + $sections_editable = !($section_storage instanceof TranslatableSectionStorageInterface && !$section_storage->isDefaultTranslation()); $output = []; if ($this->isAjax()) { @@ -118,14 +118,14 @@ protected function layout(SectionStorageInterface $section_storage) { } $count = 0; for ($i = 0; $i < $section_storage->count(); $i++) { - if (!$is_translation) { + if ($sections_editable) { $output[] = $this->buildAddSectionLink($section_storage, $count); } $output[] = $this->buildAdministrativeSection($section_storage, $count); $count++; } - if (!$is_translation) { + if ($sections_editable) { $output[] = $this->buildAddSectionLink($section_storage, $count); } @@ -158,7 +158,7 @@ protected function prepareLayout(SectionStorageInterface $section_storage) { // changed. This should avoid data loss if the layout has been // updated since this layout override has started. This probably also // needs to be done on save to avoid overriding the layout if it was - // save since the last time this page was opened. + // saved since the last time this page was opened. } } // If the layout is an override that has not yet been overridden, copy the @@ -253,8 +253,7 @@ protected function buildAdministrativeSection(SectionStorageInterface $section_s $storage_type = $section_storage->getStorageType(); $storage_id = $section_storage->getStorageId(); $section = $section_storage->getSection($delta); - $is_translation = $section_storage instanceof TranslatableSectionStorageInterface && !$section_storage->isDefaultTranslation(); - $contextual_link_group = $is_translation ? 'layout_builder_block_translation' : 'layout_builder_block'; + $sections_editable = !($section_storage instanceof TranslatableSectionStorageInterface && !$section_storage->isDefaultTranslation()); $layout = $section->getLayout(); $build = $section->toRenderArray($this->getAvailableContexts($section_storage), TRUE); $layout_definition = $layout->getPluginDefinition(); @@ -263,56 +262,58 @@ protected function buildAdministrativeSection(SectionStorageInterface $section_s foreach ($layout_definition->getRegions() as $region => $info) { if (!empty($build[$region])) { foreach (Element::children($build[$region]) as $uuid) { - if (!$is_translation) { + if ($sections_editable) { $build[$region][$uuid]['#attributes']['class'][] = 'draggable'; } - $component = $section->getComponent($uuid); - - $needs_translation_link = $this->defaultComponentHasTranslatableSettings($section_storage, $component); $build[$region][$uuid]['#attributes']['data-layout-block-uuid'] = $uuid; - if (!$is_translation || $needs_translation_link) { + $contextual_link_settings = [ + 'route_parameters' => [ + 'section_storage_type' => $storage_type, + 'section_storage' => $storage_id, + 'delta' => $delta, + 'region' => $region, + 'uuid' => $uuid, + ], + ]; + if ($sections_editable) { $build[$region][$uuid]['#contextual_links'] = [ - $contextual_link_group => [ - 'route_parameters' => [ - 'section_storage_type' => $storage_type, - 'section_storage' => $storage_id, - 'delta' => $delta, - 'region' => $region, - 'uuid' => $uuid, - ], - ], + 'layout_builder_block' => $contextual_link_settings, + ]; + } + elseif ($this->defaultComponentHasTranslatableSettings($section_storage, $component = $section->getComponent($uuid))) { + $build[$region][$uuid]['#contextual_links'] = [ + 'layout_builder_block_translation' => $contextual_link_settings, ]; } } } - if (!$is_translation) { - $build[$region]['layout_builder_add_block']['link'] = [ - '#type' => 'link', - // Add one to the current delta since it is zero-indexed. - '#title' => $this->t('Add Block in section @section, @region region', ['@section' => $delta + 1, '@region' => $region_labels[$region]]), - '#url' => Url::fromRoute('layout_builder.choose_block', - [ - 'section_storage_type' => $storage_type, - 'section_storage' => $storage_id, - 'delta' => $delta, - 'region' => $region, - ], - [ - 'attributes' => [ - 'class' => [ - 'use-ajax', - 'layout-builder__link', - 'layout-builder__link--add', - ], - 'data-dialog-type' => 'dialog', - 'data-dialog-renderer' => 'off_canvas', + $build[$region]['layout_builder_add_block']['link'] = [ + '#type' => 'link', + '#access' => $sections_editable, + // Add one to the current delta since it is zero-indexed. + '#title' => $this->t('Add Block in section @section, @region region', ['@section' => $delta + 1, '@region' => $region_labels[$region]]), + '#url' => Url::fromRoute('layout_builder.choose_block', + [ + 'section_storage_type' => $storage_type, + 'section_storage' => $storage_id, + 'delta' => $delta, + 'region' => $region, + ], + [ + 'attributes' => [ + 'class' => [ + 'use-ajax', + 'layout-builder__link', + 'layout-builder__link--add', ], - ] - ), - ]; - } + 'data-dialog-type' => 'dialog', + 'data-dialog-renderer' => 'off_canvas', + ], + ] + ), + ]; $build[$region]['layout_builder_add_block']['#type'] = 'container'; $build[$region]['layout_builder_add_block']['#attributes'] = ['class' => ['layout-builder__add-block']]; $build[$region]['layout_builder_add_block']['#weight'] = 1000; @@ -327,60 +328,59 @@ protected function buildAdministrativeSection(SectionStorageInterface $section_s $build['#attributes']['data-layout-delta'] = $delta; $build['#attributes']['class'][] = 'layout-builder__layout'; - $section_container = [ + return [ '#type' => 'container', '#attributes' => [ 'class' => ['layout-builder__section'], ], - 'layout-builder__section' => $build, - ]; - if (!$is_translation) { - $section_container += [ - 'remove' => [ - '#type' => 'link', - '#title' => $this->t('Remove section @section', ['@section' => $delta + 1]), - '#url' => Url::fromRoute('layout_builder.remove_section', [ - 'section_storage_type' => $storage_type, - 'section_storage' => $storage_id, - 'delta' => $delta, - ]), - '#attributes' => [ - 'class' => [ - 'use-ajax', - 'layout-builder__link', - 'layout-builder__link--remove', - ], - 'data-dialog-type' => 'dialog', - 'data-dialog-renderer' => 'off_canvas', + 'remove' => [ + '#type' => 'link', + '#access' => $sections_editable, + '#title' => $this->t('Remove section @section', ['@section' => $delta + 1]), + '#url' => Url::fromRoute('layout_builder.remove_section', [ + 'section_storage_type' => $storage_type, + 'section_storage' => $storage_id, + 'delta' => $delta, + ]), + '#attributes' => [ + 'class' => [ + 'use-ajax', + 'layout-builder__link', + 'layout-builder__link--remove', ], + 'data-dialog-type' => 'dialog', + 'data-dialog-renderer' => 'off_canvas', ], - 'configure' => [ - '#type' => 'link', - '#title' => $this->t('Configure section'), - '#access' => $layout instanceof PluginFormInterface, - '#url' => Url::fromRoute('layout_builder.configure_section', [ - 'section_storage_type' => $storage_type, - 'section_storage' => $storage_id, - 'delta' => $delta, - ]), - '#attributes' => [ - 'class' => [ - 'use-ajax', - 'layout-builder__link', - 'layout-builder__link--configure', - ], - 'data-dialog-type' => 'dialog', - 'data-dialog-renderer' => 'off_canvas', + ], + 'configure' => [ + '#type' => 'link', + '#title' => $this->t('Configure section'), + '#access' => $layout instanceof PluginFormInterface && $sections_editable, + '#url' => Url::fromRoute('layout_builder.configure_section', [ + 'section_storage_type' => $storage_type, + 'section_storage' => $storage_id, + 'delta' => $delta, + ]), + '#attributes' => [ + 'class' => [ + 'use-ajax', + 'layout-builder__link', + 'layout-builder__link--configure', ], + 'data-dialog-type' => 'dialog', + 'data-dialog-renderer' => 'off_canvas', ], - ]; - } - return $section_container; + ], + 'layout-builder__section' => $build, + ]; } /** * Determines if the component in the default translation is translatable. * + * @todo determine how handle other settings that need to be translated + * such as the inline blocks use case. + * * @param \Drupal\layout_builder\SectionStorageInterface $section_storage * The section storage. * @param \Drupal\layout_builder\SectionComponent $component diff --git a/core/modules/layout_builder/src/EventSubscriber/ComponentPluginLabelTranslate.php b/core/modules/layout_builder/src/EventSubscriber/ComponentPluginLabelTranslate.php index 88fcf6e124..fd489458ef 100644 --- a/core/modules/layout_builder/src/EventSubscriber/ComponentPluginLabelTranslate.php +++ b/core/modules/layout_builder/src/EventSubscriber/ComponentPluginLabelTranslate.php @@ -29,13 +29,13 @@ public static function getSubscribedEvents() { public function onBuildRender(SectionComponentBuildRenderArrayEvent $event) { $plugin = $event->getPlugin(); $contexts = $event->getContexts(); - if (!$plugin instanceof ConfigurableInterface && !isset($contexts['@language.current_language_context:language_interface'])) { + if (!$plugin instanceof ConfigurableInterface && !isset($contexts['layout_builder.entity'])) { return; } - /** @var \Drupal\Core\Language\Language $language */ - $language = $contexts['@language.current_language_context:language_interface']->getContextValue(); - $langcode = $language->getId(); + /** @var \Drupal\Core\Entity\EntityInterface $entity */ + $entity = $contexts['layout_builder.entity']->getContextValue(); + $langcode = $entity->language()->getId(); $configuration = $plugin->getConfiguration(); if (isset($configuration['label']) && isset($configuration['layout_builder_translations'][$langcode]['label'])) { $configuration['label'] = $configuration['layout_builder_translations'][$langcode]['label']; diff --git a/core/modules/layout_builder/src/Form/BlockPluginTranslationForm.php b/core/modules/layout_builder/src/Form/BlockPluginTranslationForm.php index 93ba86a782..af701ada2d 100644 --- a/core/modules/layout_builder/src/Form/BlockPluginTranslationForm.php +++ b/core/modules/layout_builder/src/Form/BlockPluginTranslationForm.php @@ -6,7 +6,6 @@ use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Plugin\PluginFormBase; -use Drupal\Core\Render\Element; use Drupal\Core\StringTranslation\StringTranslationTrait; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -17,15 +16,21 @@ class BlockPluginTranslationForm extends PluginFormBase implements ContainerInje use StringTranslationTrait; - protected $current_language; + /** + * The current language code. + * + * @var string + */ + protected $currentLangcode; /** * BlockPluginTranslationForm constructor. * - * @param $current_language + * @param string $current_langcode + * The current language code. */ - public function __construct($current_language) { - $this->current_language = $current_language; + public function __construct($current_langcode) { + $this->currentLangcode = $current_langcode; } @@ -44,12 +49,12 @@ public static function create(ContainerInterface $container) { public function buildConfigurationForm(array $form, FormStateInterface $form_state) { if ($this->plugin instanceof ConfigurableInterface) { $configuration = $this->plugin->getConfiguration(); - $form['translated_label'] = [ - '#title' => $this->t('Translated label'), - '#type' => 'textfield', - '#default_value' => isset($configuration['layout_builder_translations'][$this->current_language]['label']) ? $configuration['layout_builder_translations'][$this->current_language]['label'] : $configuration['label'], - '#required' => TRUE, - ]; + $form['translated_label'] = [ + '#title' => $this->t('Label'), + '#type' => 'textfield', + '#default_value' => isset($configuration['layout_builder_translations'][$this->currentLangcode]['label']) ? $configuration['layout_builder_translations'][$this->currentLangcode]['label'] : $configuration['label'], + '#required' => TRUE, + ]; } return $form; } @@ -66,19 +71,9 @@ public function validateConfigurationForm(array &$form, FormStateInterface $form public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { if ($this->plugin instanceof ConfigurableInterface) { $configuration = $this->plugin->getConfiguration(); - $configuration['layout_builder_translations'][$this->current_language]['label'] = $form_state->getValue('translated_label'); + $configuration['layout_builder_translations'][$this->currentLangcode]['label'] = $form_state->getValue('translated_label'); $this->plugin->setConfiguration($configuration); } } - /** - * - * @return \Drupal\Core\Plugin\PluginFormInterface - */ - protected function getConfigureForm() { - /** @var \Drupal\Core\Plugin\PluginFormFactoryInterface $form_factory */ - $form_factory = \Drupal::service('plugin_form.factory'); - return $form_factory->createInstance($this->plugin, 'configure'); - } - } diff --git a/core/modules/layout_builder/src/Form/TranslateBlockForm.php b/core/modules/layout_builder/src/Form/TranslateBlockForm.php index 67d845fe6c..e957ad7f59 100644 --- a/core/modules/layout_builder/src/Form/TranslateBlockForm.php +++ b/core/modules/layout_builder/src/Form/TranslateBlockForm.php @@ -16,7 +16,7 @@ class TranslateBlockForm extends ConfigureBlockFormBase { * {@inheritdoc} */ protected function submitLabel() { - $this->t('Save'); + return $this->t('Translate'); } /** diff --git a/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php index 6b9954d036..e8520bf968 100644 --- a/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php @@ -409,8 +409,6 @@ public function isDefaultTranslation() { $entity = $this->getEntity(); return $entity->isDefaultTranslation(); } - // @todo If not translatable should we always consider this the default - // translation? return TRUE; } diff --git a/core/modules/layout_builder/tests/src/Functional/LayoutBuilderMultilingualTest.php b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderMultilingualTest.php index 683d6012df..f832d27ca0 100644 --- a/core/modules/layout_builder/tests/src/Functional/LayoutBuilderMultilingualTest.php +++ b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderMultilingualTest.php @@ -21,7 +21,7 @@ class LayoutBuilderMultilingualTest extends BrowserTestBase { 'layout_builder', 'node', 'block_content', - 'content_translation', + 'language', 'locale', ]; @@ -50,7 +50,8 @@ protected function setUp() { $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() @@ -60,14 +61,23 @@ protected function setUp() { ConfigurableLanguage::createFromLangcode('es')->save(); // Create a node and translate it. - $node = $this->createNode([ + $en_node = $this->createNode([ 'type' => 'bundle_with_section_field', 'title' => 'The untranslated node title', ]); - $node->addTranslation('es', [ - 'title' => 'The translated node title', + $en_node->save(); + + // Create a second node in Spanish to confirm the UI works in another + // language. We cannot use a translation for this because layout + // translations do not have the full UI. + $es_node = $this->createNode([ + 'type' => 'bundle_with_section_field', + 'title' => 'The untranslated node title', + 'langcode' => 'es', ]); - $node->save(); + $es_node->save(); + + $this->drupalLogin($this->createUser([ 'configure any layout', @@ -81,7 +91,7 @@ protected function setUp() { public function testCustomBlocks() { // Check translated and untranslated entities before translating the string. $this->assertCustomBlocks('node/1'); - $this->assertCustomBlocks('es/node/1'); + $this->assertCustomBlocks('es/node/2'); // Translate the 'Inline blocks' string used as a category in // \Drupal\layout_builder\Controller\ChooseBlockController::inlineBlockList(). @@ -90,7 +100,7 @@ public function testCustomBlocks() { // Check translated and untranslated entities after translating the string. $this->assertCustomBlocks('node/1'); - $this->assertCustomBlocks('es/node/1'); + $this->assertCustomBlocks('es/node/2'); } /** diff --git a/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTranslationTest.php b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTranslationTest.php index 841d4867ea..3d9a18279e 100644 --- a/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTranslationTest.php +++ b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTranslationTest.php @@ -3,6 +3,8 @@ 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 UI works with translated content. @@ -97,7 +99,8 @@ public function testLayoutPerTranslation() { $assert_session->pageTextContains('The translated field value'); $assert_session->pageTextContains('Powered by Drupal'); - // The translate layout is not available. + // Confirm that layout translation page is accessible once the untranslated + // entity has a override. $this->drupalGet($translated_layout_url); $assert_session->pageTextNotContains('Access denied'); $assert_session->pageTextNotContains('The untranslated field value'); @@ -105,10 +108,8 @@ public function testLayoutPerTranslation() { $assert_session->pageTextContains('Powered by Drupal'); $assert_session->buttonExists('Save layout'); - // Confirm that links do not exist to change the layout. - $assert_session->linkNotExists('Add Section'); - $assert_session->linkNotExists('Add Block'); - $assert_session->linkNotExists('Remove section'); + $this->assertNonTranslationActionsRemoved(); + } @@ -139,4 +140,79 @@ public function testLayoutTranslationNoOverride() { $assert_session->pageTextContains('Access denied'); } + /** + * The entity used for testing. + * + * @var \Drupal\Core\Entity\EntityInterface + */ + protected $entity; + + /** + * {@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); + + // Create a translation. + $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'); + } + + /** + * 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(); + } + } diff --git a/core/modules/layout_builder/tests/src/Functional/LayoutSectionTest.php b/core/modules/layout_builder/tests/src/Functional/LayoutSectionTest.php index 1041dd813e..e5b70c0b3f 100644 --- a/core/modules/layout_builder/tests/src/Functional/LayoutSectionTest.php +++ b/core/modules/layout_builder/tests/src/Functional/LayoutSectionTest.php @@ -203,13 +203,12 @@ public function testLayoutSectionFormatterAccess() { * Tests the multilingual support of the section formatter. */ public function testMultilingualLayoutSectionFormatter() { - $this->container->get('module_installer')->install(['content_translation']); + $this->container->get('module_installer')->install(['language']); $this->rebuildContainer(); ConfigurableLanguage::createFromLangcode('es')->save(); - $this->container->get('content_translation.manager')->setEnabled('node', 'bundle_with_section_field', TRUE); - $entity = $this->createSectionNode([ + $en_entity = $this->createSectionNode([ [ 'section' => new Section('layout_onecol', [], [ 'baz' => new SectionComponent('baz', 'content', [ @@ -218,28 +217,27 @@ public function testMultilingualLayoutSectionFormatter() { ]), ], ]); - $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', - ]), + + $es_entity = $this->createSectionNode([ + [ + '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(); + $es_entity->set('langcode', 'es'); + $es_entity->save(); - $this->drupalGet($entity->toUrl('canonical')); + $this->drupalGet($en_entity->toUrl('canonical')); $this->assertLayoutSection('.layout--onecol', 'Powered by'); - $this->drupalGet($entity->toUrl('canonical')->setOption('prefix', 'es/')); + $this->drupalGet($es_entity->toUrl('canonical')); $this->assertLayoutSection('.layout--twocol', ['foo text', 'bar text']); } diff --git a/core/modules/layout_builder/tests/src/Functional/TranslationTestTrait.php b/core/modules/layout_builder/tests/src/Functional/TranslationTestTrait.php index a7d33d2a1a..cbf54dd9e9 100644 --- a/core/modules/layout_builder/tests/src/Functional/TranslationTestTrait.php +++ b/core/modules/layout_builder/tests/src/Functional/TranslationTestTrait.php @@ -2,8 +2,6 @@ namespace Drupal\Tests\layout_builder\Functional; -use Drupal\Core\Entity\Entity\EntityViewDisplay; -use Drupal\Core\Url; /** * Common functions for testing Layout Builder with translations. @@ -11,78 +9,16 @@ trait TranslationTestTrait { /** - * The entity used for testing. + * @param \Drupal\Tests\WebAssert $assert_session * - * @var \Drupal\Core\Entity\EntityInterface + * @throws \Behat\Mink\Exception\ExpectationException */ - protected $entity; - - /** - * {@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); - - // Create a translation. - $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'); - } - - /** - * 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(); + protected function assertNonTranslationActionsRemoved() { + $assert_session = $this->assertSession(); + // Confirm that links do not exist to change the layout. + $assert_session->linkNotExists('Add Section'); + $assert_session->linkNotExists('Add Block'); + $assert_session->linkNotExists('Remove section'); } } diff --git a/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderDisableInteractionsTest.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderDisableInteractionsTest.php index e4b342ba86..1b55d8af7e 100644 --- a/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderDisableInteractionsTest.php +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderDisableInteractionsTest.php @@ -18,6 +18,7 @@ class LayoutBuilderDisableInteractionsTest extends WebDriverTestBase { use ContextualLinkClickTrait; + use LayoutBuilderTestTrait; /** * {@inheritdoc} @@ -133,36 +134,6 @@ public function testFormsLinksDisabled() { $this->assertContextualLinksClickable(); } - /** - * Adds a block in the Layout Builder. - * - * @param string $block_link_text - * The link text to add the block. - * @param string $rendered_locator - * The CSS locator to confirm the block was rendered. - */ - protected function addBlock($block_link_text, $rendered_locator) { - $assert_session = $this->assertSession(); - $page = $this->getSession()->getPage(); - - // Add a new block. - $this->assertNotEmpty($assert_session->waitForElementVisible('css', '#layout-builder a:contains(\'Add Block\')')); - $this->clickLink('Add Block'); - $this->assertNotEmpty($assert_session->waitForElementVisible('css', '#drupal-off-canvas')); - $assert_session->assertWaitOnAjaxRequest(); - - $assert_session->linkExists($block_link_text); - $this->clickLink($block_link_text); - - // Wait for off-canvas dialog to reopen with block form. - $this->assertNotEmpty($assert_session->waitForElementVisible('css', ".layout-builder-add-block")); - $assert_session->assertWaitOnAjaxRequest(); - $page->pressButton('Add Block'); - - // Wait for block form to be rendered in the Layout Builder. - $this->assertNotEmpty($assert_session->waitForElement('css', $rendered_locator)); - } - /** * Checks if element is unclickable. * @@ -275,26 +246,4 @@ protected function movePointerTo($selector) { $driver_session->moveto(['element' => $element->getID()]); } - /** - * Waits for an element to be removed from the page. - * - * @param string $selector - * CSS selector. - * @param int $timeout - * (optional) Timeout in milliseconds, defaults to 10000. - * @param string $message - * (optional) Custom message to display with the assertion. - * - * @todo: Remove after https://www.drupal.org/project/drupal/issues/2892440 - */ - public function assertNoElementAfterWait($selector, $timeout = 10000, $message = '') { - $page = $this->getSession()->getPage(); - if ($message === '') { - $message = "Element '$selector' was not on the page after wait."; - } - $this->assertTrue($page->waitFor($timeout / 1000, function () use ($page, $selector) { - return empty($page->find('css', $selector)); - }), $message); - } - } diff --git a/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderTestTrait.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderTestTrait.php new file mode 100644 index 0000000000..0568f869fd --- /dev/null +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/LayoutBuilderTestTrait.php @@ -0,0 +1,66 @@ +assertSession(); + $page = $this->getSession()->getPage(); + + // Add a new block. + $this->assertNotEmpty($assert_session->waitForElementVisible('css', '#layout-builder a:contains(\'Add Block\')')); + $this->clickLink('Add Block'); + $this->assertNotEmpty($assert_session->waitForElementVisible('css', '#drupal-off-canvas')); + $assert_session->assertWaitOnAjaxRequest(); + + $assert_session->linkExists($block_link_text); + $this->clickLink($block_link_text); + + // Wait for off-canvas dialog to reopen with block form. + $this->assertNotEmpty($assert_session->waitForElementVisible('css', ".layout-builder-add-block")); + $assert_session->assertWaitOnAjaxRequest(); + if ($label_display) { + $page->checkField('settings[label_display]'); + } + if ($label !== NULL) { + $page->fillField('settings[label]', $label); + } + $page->pressButton('Add Block'); + + // Wait for block form to be rendered in the Layout Builder. + $this->assertNotEmpty($assert_session->waitForElement('css', $rendered_locator)); + } + + /** + * Waits for an element to be removed from the page. + * + * @param string $selector + * CSS selector. + * @param int $timeout + * (optional) Timeout in milliseconds, defaults to 10000. + * @param string $message + * (optional) Custom message to display with the assertion. + * + * @todo: Remove after https://www.drupal.org/project/drupal/issues/2892440 + */ + public function assertNoElementAfterWait($selector, $timeout = 10000, $message = '') { + $page = $this->getSession()->getPage(); + if ($message === '') { + $message = "Element '$selector' was not on the page after wait."; + } + $this->assertTrue($page->waitFor($timeout / 1000, function () use ($page, $selector) { + return empty($page->find('css', $selector)); + }), $message); + } + +} diff --git a/core/modules/layout_builder/tests/src/FunctionalJavascript/TranslationTest.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/TranslationTest.php new file mode 100644 index 0000000000..f74289b1a4 --- /dev/null +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/TranslationTest.php @@ -0,0 +1,140 @@ +drupalPlaceBlock('local_tasks_block'); + + $this->createContentType(['type' => 'bundle_with_section_field', 'new_revision' => TRUE]); + $this->createNode([ + 'type' => 'bundle_with_section_field', + 'title' => 'The node title', + 'body' => [ + [ + 'value' => 'The node body', + ], + ], + ]); + // Adds a new language. + ConfigurableLanguage::createFromLangcode('it')->save(); + + // Enable translation for the node type 'bundle_with_section_field'. + \Drupal::service('content_translation.manager')->setEnabled('node', 'bundle_with_section_field', TRUE); + drupal_static_reset(); + \Drupal::entityTypeManager()->clearCachedDefinitions(); + \Drupal::service('router.builder')->rebuild(); + \Drupal::service('entity.definition_update_manager')->applyUpdates(); + + $this->rebuildContainer(); + } + + /** + * Tests that block labels can be translated. + */ + public function testLabelTranslation() { + $page = $this->getSession()->getPage(); + $assert_session = $this->assertSession(); + + $this->drupalLogin($this->drupalCreateUser([ + 'access contextual links', + 'configure any layout', + 'administer node display', + 'administer node fields', + 'translate bundle_with_section_field node', + 'create content translations', + ])); + + // Allow layout overrides. + $this->drupalPostForm( + static::FIELD_UI_PREFIX . '/display/default', + ['layout[enabled]' => TRUE], + 'Save' + ); + $this->drupalPostForm( + static::FIELD_UI_PREFIX . '/display/default', + ['layout[allow_custom]' => TRUE], + 'Save' + ); + + // Add a new inline block to the original node. + $this->drupalGet('node/1/layout'); + $this->addBlock('Powered by Drupal', '.block-system-powered-by-block', TRUE, 'untranslated label'); + $assert_session->pageTextContains('untranslated label'); + $assert_session->buttonExists('Save layout'); + $page->pressButton('Save layout'); + $assert_session->addressEquals('node/1'); + + + // Create a translation. + $add_translation_url = Url::fromRoute("entity.node.content_translation_add", [ + 'node' => 1, + 'source' => 'en', + 'target' => 'it', + ]); + $this->drupalPostForm($add_translation_url, [ + 'title[0][value]' => 'The translated node title', + 'body[0][value]' => 'The translated node body', + ], 'Save'); + + // Update the translations block label. + $this->drupalGet('it/node/1/layout'); + $this->assertNonTranslationActionsRemoved(); + $this->clickContextualLink('.block-system-powered-by-block', 'Translate block'); + $label_input = $assert_session->waitForElementVisible('css', '#drupal-off-canvas [name="settings[translated_label]"]'); + $this->assertNotEmpty($label_input); + $this->assertEquals('untranslated label', $label_input->getValue()); + $label_input->setValue('label in translation'); + $page->pressButton('Translate'); + $this->assertNoElementAfterWait('#drupal-off-canvas'); + $this->assertNotEmpty($assert_session->waitForElementVisible('css', 'h2:contains("label in translation")')); + $assert_session->buttonExists('Save layout'); + $page->pressButton('Save layout'); + $assert_session->addressEquals('it/node/1'); + $assert_session->pageTextContains('label in translation'); + + // Confirm that untranslated label is still used on default translation. + $this->drupalGet('node/1'); + $assert_session->pageTextContains('untranslated label'); + $assert_session->pageTextNotContains('label in translation'); + } + +} diff --git a/core/modules/layout_builder/tests/src/Unit/OverridesSectionStorageTest.php b/core/modules/layout_builder/tests/src/Unit/OverridesSectionStorageTest.php index 0b90811995..4ff8a1a0a2 100644 --- a/core/modules/layout_builder/tests/src/Unit/OverridesSectionStorageTest.php +++ b/core/modules/layout_builder/tests/src/Unit/OverridesSectionStorageTest.php @@ -127,7 +127,6 @@ public function providerTestExtractIdFromRoute() { public function testGetSectionListFromId($success, $expected_entity_type_id, $id) { $defaults['the_parameter_name'] = $id; - $this->entityRepository->getTranslationFromContext(Argument::cetera())->shouldNotBeCalled(); if ($expected_entity_type_id) { $entity_without_layout = $this->prophesize(FieldableEntityInterface::class); $entity_without_layout->hasField(OverridesSectionStorage::FIELD_NAME)->willReturn(FALSE);