diff --git a/core/modules/layout_builder/layout_builder.links.contextual.yml b/core/modules/layout_builder/layout_builder.links.contextual.yml index bcf2a9cf06..4cff0d197b 100644 --- a/core/modules/layout_builder/layout_builder.links.contextual.yml +++ b/core/modules/layout_builder/layout_builder.links.contextual.yml @@ -17,3 +17,14 @@ layout_builder_block_remove: class: ['use-ajax'] data-dialog-type: dialog data-dialog-renderer: off_canvas + +layout_builder_block_translate: + title: 'Translate block' + route_name: 'layout_builder.translate_block' + group: 'layout_builder_block_translation' + options: + attributes: + class: ['use-ajax'] + data-dialog-type: dialog + data-dialog-renderer: off_canvas + diff --git a/core/modules/layout_builder/layout_builder.module b/core/modules/layout_builder/layout_builder.module index 277f81a87c..e3f2e13199 100644 --- a/core/modules/layout_builder/layout_builder.module +++ b/core/modules/layout_builder/layout_builder.module @@ -25,6 +25,7 @@ use Drupal\layout_builder\InlineBlockEntityOperations; use Drupal\Core\Session\AccountInterface; use Drupal\Core\Access\AccessResult; +use Drupal\layout_builder\Form\BlockPluginTranslationForm; use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage; /** @@ -323,10 +324,24 @@ function layout_builder_entity_translation_create(EntityInterface $translation) $translatable_fields = $translation->getTranslatableFields(); if (array_key_exists(OverridesSectionStorage::FIELD_NAME, $translatable_fields)) { // When creating a new translation do not copy untranslated sections. - // Until the user explicitly creates an layout translation the translated - // layout will use the untranslated layout. - // @see \Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay::buildSections() + // Currently totally independent overrides per language are not supported. $translation->set(OverridesSectionStorage::FIELD_NAME, []); } } } + +/** + * Implements hook_block_alter(). + * + * Ensures every block plugin definition has an 'layout_builder_translation' + * form specified. + */ +function layout_builder_block_alter(&$definitions) { + foreach ($definitions as &$definition) { + // If a block plugin does not define its own 'layout_builder_translation' + // form use the one provided by this module. + if (!isset($definition['forms']['layout_builder_translation'])) { + $definition['forms']['layout_builder_translation'] = BlockPluginTranslationForm::class; + } + } +} diff --git a/core/modules/layout_builder/layout_builder.routing.yml b/core/modules/layout_builder/layout_builder.routing.yml index b436055378..f034a5a1b2 100644 --- a/core/modules/layout_builder/layout_builder.routing.yml +++ b/core/modules/layout_builder/layout_builder.routing.yml @@ -6,6 +6,7 @@ layout_builder.choose_section: requirements: _permission: 'configure any layout' _layout_builder_access: 'view' + _layout_builder_translation_access: 'view' options: _admin_route: TRUE parameters: @@ -19,6 +20,7 @@ layout_builder.add_section: requirements: _permission: 'configure any layout' _layout_builder_access: 'view' + _layout_builder_translation_access: 'view' options: _admin_route: TRUE parameters: @@ -36,6 +38,7 @@ layout_builder.configure_section: requirements: _permission: 'configure any layout' _layout_builder_access: 'view' + _layout_builder_translation_access: 'view' options: _admin_route: TRUE parameters: @@ -49,6 +52,7 @@ layout_builder.remove_section: requirements: _permission: 'configure any layout' _layout_builder_access: 'view' + _layout_builder_translation_access: 'view' options: _admin_route: TRUE parameters: @@ -63,6 +67,7 @@ layout_builder.choose_block: requirements: _permission: 'configure any layout' _layout_builder_access: 'view' + _layout_builder_translation_access: 'view' options: _admin_route: TRUE parameters: @@ -77,6 +82,7 @@ layout_builder.add_block: requirements: _permission: 'configure any layout' _layout_builder_access: 'view' + _layout_builder_translation_access: 'view' options: _admin_route: TRUE parameters: @@ -90,6 +96,7 @@ layout_builder.choose_inline_block: _title: 'Add a new Inline Block' requirements: _permission: 'configure any layout' + _layout_builder_translation_access: 'view' options: _admin_route: TRUE parameters: @@ -101,6 +108,21 @@ layout_builder.update_block: defaults: _form: '\Drupal\layout_builder\Form\UpdateBlockForm' _title: 'Configure block' + requirements: + _permission: 'configure any layout' + _layout_builder_access: 'view' + _layout_builder_translation_access: 'view' + options: + _admin_route: TRUE + parameters: + section_storage: + layout_builder_tempstore: TRUE + +layout_builder.translate_block: + path: '/layout_builder/translate/block/{section_storage_type}/{section_storage}/{delta}/{region}/{uuid}' + defaults: + _form: '\Drupal\layout_builder\Form\TranslateBlockForm' + _title: 'Translate block' requirements: _permission: 'configure any layout' _layout_builder_access: 'view' @@ -117,6 +139,7 @@ layout_builder.remove_block: requirements: _permission: 'configure any layout' _layout_builder_access: 'view' + _layout_builder_translation_access: 'view' options: _admin_route: TRUE parameters: @@ -136,6 +159,7 @@ layout_builder.move_block: requirements: _permission: 'configure any layout' _layout_builder_access: 'view' + _layout_builder_translation_access: 'view' options: _admin_route: TRUE parameters: diff --git a/core/modules/layout_builder/layout_builder.services.yml b/core/modules/layout_builder/layout_builder.services.yml index 65fb66a895..6f1f87571f 100644 --- a/core/modules/layout_builder/layout_builder.services.yml +++ b/core/modules/layout_builder/layout_builder.services.yml @@ -6,6 +6,10 @@ services: class: Drupal\layout_builder\Access\LayoutBuilderAccessCheck tags: - { name: access_check, applies_to: _layout_builder_access } + access_check.entity.layout_builder_translation_access: + class: Drupal\layout_builder\Access\LayoutBuilderTranslationAccessCheck + tags: + - { name: access_check, applies_to: _layout_builder_translation_access } access_check.entity.layout: class: Drupal\layout_builder\Access\LayoutSectionAccessCheck tags: @@ -37,6 +41,10 @@ services: arguments: ['@current_user'] tags: - { name: event_subscriber } + layout_builder.translate_block_component_subscriber: + class: Drupal\layout_builder\EventSubscriber\ComponentPluginLabelTranslate + tags: + - { name: event_subscriber } logger.channel.layout_builder: parent: logger.channel_base arguments: ['layout_builder'] diff --git a/core/modules/layout_builder/src/Access/LayoutBuilderTranslationAccessCheck.php b/core/modules/layout_builder/src/Access/LayoutBuilderTranslationAccessCheck.php new file mode 100644 index 0000000000..f3df40112a --- /dev/null +++ b/core/modules/layout_builder/src/Access/LayoutBuilderTranslationAccessCheck.php @@ -0,0 +1,37 @@ +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 df34477ab1..9b05fe9d26 100644 --- a/core/modules/layout_builder/src/Element/LayoutBuilder.php +++ b/core/modules/layout_builder/src/Element/LayoutBuilder.php @@ -2,6 +2,7 @@ namespace Drupal\layout_builder\Element; +use Drupal\Component\Plugin\ConfigurableInterface; use Drupal\Core\Ajax\AjaxHelperTrait; use Drupal\Core\Messenger\MessengerInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; @@ -12,8 +13,9 @@ use Drupal\layout_builder\Context\LayoutBuilderContextTrait; use Drupal\layout_builder\LayoutTempstoreRepositoryInterface; use Drupal\layout_builder\OverridesSectionStorageInterface; -use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage; +use Drupal\layout_builder\SectionComponent; use Drupal\layout_builder\SectionStorageInterface; +use Drupal\layout_builder\TranslatableSectionStorageInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -106,6 +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(); $output = []; if ($this->isAjax()) { @@ -115,17 +118,27 @@ protected function layout(SectionStorageInterface $section_storage) { } $count = 0; for ($i = 0; $i < $section_storage->count(); $i++) { - $output[] = $this->buildAddSectionLink($section_storage, $count); + if (!$is_translation) { + $output[] = $this->buildAddSectionLink($section_storage, $count); + } + $output[] = $this->buildAdministrativeSection($section_storage, $count); $count++; } - $output[] = $this->buildAddSectionLink($section_storage, $count); + if (!$is_translation) { + $output[] = $this->buildAddSectionLink($section_storage, $count); + } + $output['#attached']['library'][] = 'layout_builder/drupal.layout_builder'; $output['#type'] = 'container'; $output['#attributes']['id'] = 'layout-builder'; $output['#attributes']['class'][] = 'layout-builder'; // Mark this UI as uncacheable. $output['#cache']['max-age'] = 0; + + // @todo Add message if not components have translate links! + // "There are no settings to translate" + return $output; } @@ -139,17 +152,20 @@ protected function prepareLayout(SectionStorageInterface $section_storage) { // If the layout has pending changes, add a warning. if ($this->layoutTempstoreRepository->has($section_storage)) { $this->messenger->addWarning($this->t('You have unsaved changes.')); + if ($section_storage instanceof TranslatableSectionStorageInterface && !$section_storage->isDefaultTranslation()) { + // @todo Copy in any change from the default translation and then + // reapply any translated labels where the original labels has not + // 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. + } } // If the layout is an override that has not yet been overridden, copy the // sections from the corresponding default. elseif ($section_storage instanceof OverridesSectionStorageInterface && !$section_storage->isOverridden()) { - if ($section_storage->isTranslatable() && !$section_storage->isDefaultTranslation()) { - $source_storage = $section_storage->getDefaultTranslationSectionStorage(); - } - else { - $source_storage = $section_storage->getDefaultSectionStorage(); - } - foreach ($source_storage->getSections() as $section) { + $sections = $section_storage->getDefaultSectionStorage()->getSections(); + foreach ($sections as $section) { $section_storage->appendSection($section); } $this->layoutTempstoreRepository->set($section_storage); @@ -237,7 +253,8 @@ 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'; $layout = $section->getLayout(); $build = $section->toRenderArray($this->getAvailableContexts($section_storage), TRUE); $layout_definition = $layout->getPluginDefinition(); @@ -246,46 +263,56 @@ protected function buildAdministrativeSection(SectionStorageInterface $section_s foreach ($layout_definition->getRegions() as $region => $info) { if (!empty($build[$region])) { foreach (Element::children($build[$region]) as $uuid) { - $build[$region][$uuid]['#attributes']['class'][] = 'draggable'; + if (!$is_translation) { + $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; - $build[$region][$uuid]['#contextual_links'] = [ - 'layout_builder_block' => [ - 'route_parameters' => [ - 'section_storage_type' => $storage_type, - 'section_storage' => $storage_id, - 'delta' => $delta, - 'region' => $region, - 'uuid' => $uuid, + if (!$is_translation || $needs_translation_link) { + $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, + ], ], - ], - ]; + ]; + } } } - $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', + 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']['#type'] = 'container'; $build[$region]['layout_builder_add_block']['#attributes'] = ['class' => ['layout-builder__add-block']]; $build[$region]['layout_builder_add_block']['#weight'] = 1000; @@ -300,50 +327,81 @@ protected function buildAdministrativeSection(SectionStorageInterface $section_s $build['#attributes']['data-layout-delta'] = $delta; $build['#attributes']['class'][] = 'layout-builder__layout'; - return [ + $section_container = [ '#type' => 'container', '#attributes' => [ 'class' => ['layout-builder__section'], ], - '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', + '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', ], - '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', + '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', ], - 'data-dialog-type' => 'dialog', - 'data-dialog-renderer' => 'off_canvas', ], - ], - 'layout-builder__section' => $build, - ]; + ]; + } + return $section_container; + } + + /** + * Determines if the component in the default translation is translatable. + * + * @param \Drupal\layout_builder\SectionStorageInterface $section_storage + * The section storage. + * @param \Drupal\layout_builder\SectionComponent $component + * The component to check. + * + * @return bool + * TRUE if the default component has translatable settings, otherwise FALSE. + */ + protected function defaultComponentHasTranslatableSettings(SectionStorageInterface $section_storage, SectionComponent $component) { + if ($section_storage instanceof TranslatableSectionStorageInterface && !$section_storage->isDefaultTranslation()) { + foreach ($section_storage->getDefaultTranslationSections() as $default_translation_section) { + if ($default_component = $default_translation_section->getComponent($component->getUuid())) { + $plugin = $default_component->getPlugin(); + if ($plugin instanceof ConfigurableInterface) { + $configuration = $plugin->getConfiguration(); + return !empty($configuration['label_display']) && !empty($configuration['label']); + } + } + } + } + return FALSE; } } diff --git a/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php index 31233cdfc3..c7ecf6d844 100644 --- a/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php +++ b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php @@ -13,7 +13,6 @@ use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; -use Drupal\layout_builder\OverridesSectionStorageInterface; use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage; use Drupal\layout_builder\Section; use Drupal\layout_builder\SectionComponent; @@ -202,6 +201,7 @@ protected function addSectionField($entity_type_id, $bundle, $field_name) { 'type' => 'layout_section', 'locked' => TRUE, ]); + $field_storage->setTranslatable(FALSE); $field_storage->save(); } @@ -289,11 +289,6 @@ protected function buildSections(FieldableEntityInterface $entity) { $cacheability = new CacheableMetadata(); $storage = $this->sectionStorageManager()->findByContext($contexts, $cacheability); - if ($storage instanceof OverridesSectionStorageInterface) { - if ($storage->isTranslatable() && !$storage->isDefaultTranslation() && !$storage->isOverridden()) { - $storage = $storage->getDefaultTranslationSectionStorage(); - } - } $build = []; if ($storage) { diff --git a/core/modules/layout_builder/src/EventSubscriber/ComponentPluginLabelTranslate.php b/core/modules/layout_builder/src/EventSubscriber/ComponentPluginLabelTranslate.php new file mode 100644 index 0000000000..88fcf6e124 --- /dev/null +++ b/core/modules/layout_builder/src/EventSubscriber/ComponentPluginLabelTranslate.php @@ -0,0 +1,46 @@ +getPlugin(); + $contexts = $event->getContexts(); + if (!$plugin instanceof ConfigurableInterface && !isset($contexts['@language.current_language_context:language_interface'])) { + return; + } + + /** @var \Drupal\Core\Language\Language $language */ + $language = $contexts['@language.current_language_context:language_interface']->getContextValue(); + $langcode = $language->getId(); + $configuration = $plugin->getConfiguration(); + if (isset($configuration['label']) && isset($configuration['layout_builder_translations'][$langcode]['label'])) { + $configuration['label'] = $configuration['layout_builder_translations'][$langcode]['label']; + $plugin->setConfiguration($configuration); + } + } + +} diff --git a/core/modules/layout_builder/src/Form/BlockPluginTranslationForm.php b/core/modules/layout_builder/src/Form/BlockPluginTranslationForm.php new file mode 100644 index 0000000000..93ba86a782 --- /dev/null +++ b/core/modules/layout_builder/src/Form/BlockPluginTranslationForm.php @@ -0,0 +1,84 @@ +current_language = $current_language; + } + + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('language_manager')->getCurrentLanguage()->getId() + ); + } + + /** + * {@inheritdoc} + */ + 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, + ]; + } + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { + } + + /** + * {@inheritdoc} + */ + 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'); + $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 new file mode 100644 index 0000000000..67d845fe6c --- /dev/null +++ b/core/modules/layout_builder/src/Form/TranslateBlockForm.php @@ -0,0 +1,63 @@ +t('Save'); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'layout_builder_block_translation'; + } + + /** + * Builds the block form. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * @param \Drupal\layout_builder\SectionStorageInterface $section_storage + * The section storage being configured. + * @param int $delta + * The delta of the section. + * @param string $region + * The region of the block. + * @param string $uuid + * The UUID of the block being updated. + * + * @return array + * The form array. + */ + public function buildForm(array $form, FormStateInterface $form_state, SectionStorageInterface $section_storage = NULL, $delta = NULL, $region = NULL, $uuid = NULL) { + $component = $section_storage->getSection($delta)->getComponent($uuid); + return $this->doBuildForm($form, $form_state, $section_storage, $delta, $component); + } + + /** + * {@inheritdoc} + */ + protected function getPluginForm(BlockPluginInterface $block) { + if ($block instanceof PluginWithFormsInterface) { + return $this->pluginFormFactory->createInstance($block, 'layout_builder_translation'); + } + return $block; + } + +} diff --git a/core/modules/layout_builder/src/InlineBlockEntityOperations.php b/core/modules/layout_builder/src/InlineBlockEntityOperations.php index 4f0648ba64..55eab3b75e 100644 --- a/core/modules/layout_builder/src/InlineBlockEntityOperations.php +++ b/core/modules/layout_builder/src/InlineBlockEntityOperations.php @@ -7,7 +7,6 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Entity\RevisionableInterface; -use Drupal\Core\Entity\TranslatableInterface; use Drupal\layout_builder\Plugin\Block\InlineBlock; use Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -177,7 +176,6 @@ public function handlePreSave(EntityInterface $entity) { // duplicated. $duplicate_blocks = TRUE; } - $new_revision = FALSE; if ($entity instanceof RevisionableInterface) { // If the parent entity will have a new revision create a new revision @@ -283,28 +281,4 @@ protected function saveInlineBlockComponent(EntityInterface $entity, SectionComp $component->setConfiguration($post_save_configuration); } - /** - * Determines if an entity is a new translation. - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity to check. - * - * @return bool - * TRUE if the entity has a new layout translation, otherwise FALSE. - */ - private function hasNewLayoutTranslation(EntityInterface $entity) { - - if (isset($entity->original) && $entity instanceof TranslatableInterface && $entity->isTranslatable() && !$entity->isDefaultTranslation()) { - // Get the unchanged translation. $entity->original will not work here. - // @todo Confirm ^ - $unchanged_translation = $this->entityTypeManager->getStorage($entity->getEntityTypeId())->loadUnchanged($entity->id())->getTranslation($entity->language()->getId()); - assert($entity->language()->getId() == $unchanged_translation->language()->getId()); - $sections = $this->getEntitySections($unchanged_translation); - if (empty($sections)) { - return TRUE; - } - } - return FALSE; - } - } diff --git a/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php index 9c222871d8..6b9954d036 100644 --- a/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php @@ -3,9 +3,7 @@ namespace Drupal\layout_builder\Plugin\SectionStorage; use Drupal\Core\Access\AccessResult; -use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Cache\RefinableCacheableDependencyInterface; -use Drupal\Core\Entity\Entity\EntityViewDisplay; use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Entity\EntityRepositoryInterface; use Drupal\Core\Entity\EntityTypeInterface; @@ -17,12 +15,11 @@ use Drupal\Core\Plugin\Context\ContextDefinition; use Drupal\Core\Plugin\Context\EntityContext; use Drupal\Core\Session\AccountInterface; -use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\Core\Url; use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay; use Drupal\layout_builder\OverridesSectionStorageInterface; use Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface; -use Drupal\layout_builder\TranslatableOverridesSectionStorageInterface; +use Drupal\layout_builder\TranslatableSectionStorageInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Routing\RouteCollection; @@ -51,7 +48,7 @@ * experimental modules and development releases of contributed modules. * See https://www.drupal.org/core/experimental for more information. */ -class OverridesSectionStorage extends SectionStorageBase implements ContainerFactoryPluginInterface, TranslatableOverridesSectionStorageInterface, SectionStorageLocalTaskProviderInterface { +class OverridesSectionStorage extends SectionStorageBase implements ContainerFactoryPluginInterface, OverridesSectionStorageInterface, TranslatableSectionStorageInterface, SectionStorageLocalTaskProviderInterface { /** * The field name used by this storage. @@ -366,11 +363,7 @@ public function save() { public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) { $default_section_storage = $this->getDefaultSectionStorage(); $result = AccessResult::allowedIf($default_section_storage->isLayoutBuilderEnabled())->addCacheableDependency($default_section_storage); - $entity = $this->getEntity(); - $result->addCacheableDependency($entity); - if ($entity instanceof TranslatableInterface && !$entity->isDefaultTranslation()) { - $result = $result->andIf(AccessResult::allowedIf($this->isTranslatable())); - } + $result = $result->andIf(AccessResult::allowedIf($this->isDefaultTranslation() || $this->isOverridden()))->addCacheableDependency($this); return $return_as_object ? $result : $result->isAllowed(); } @@ -381,18 +374,7 @@ public function isApplicable(RefinableCacheableDependencyInterface $cacheability $default_section_storage = $this->getDefaultSectionStorage(); $cacheability->addCacheableDependency($default_section_storage)->addCacheableDependency($this); // Check that overrides are enabled and have at least one section. - if ($default_section_storage->isOverridable()) { - if ($this->isOverridden()) { - return TRUE; - } - if ($this->isTranslatable() && !$this->isDefaultTranslation()) { - $default_translation_section_storage = $this->getDefaultTranslationSectionStorage(); - if ($default_translation_section_storage instanceof OverridesSectionStorageInterface) { - return $default_translation_section_storage->isOverridden(); - } - } - } - return FALSE; + return $default_section_storage->isOverridable() && $this->isOverridden(); } /** @@ -411,8 +393,9 @@ public function isOverridden() { public function isTranslatable() { $entity = $this->getEntity(); if ($entity instanceof TranslatableInterface) { - $translatable_fields = $entity->getTranslatableFields(FALSE); - return array_key_exists(static::FIELD_NAME, $translatable_fields) && $entity->isTranslatable(); + return $entity->isTranslatable(); + /*$translatable_fields = $entity->getTranslatableFields(FALSE); + return array_key_exists(static::FIELD_NAME, $translatable_fields) && $entity->isTranslatable();*/ } return FALSE; } @@ -426,43 +409,22 @@ public function isDefaultTranslation() { $entity = $this->getEntity(); return $entity->isDefaultTranslation(); } - return FALSE; + // @todo If not translatable should we always consider this the default + // translation? + return TRUE; } /** * {@inheritdoc} */ - public function getDefaultTranslationSectionStorage() { + public function getDefaultTranslationSections() { if ($this->isTranslatable()) { - if ($this->isDefaultTranslation()) { - return $this; - } - else { - /** @var \Drupal\Core\Entity\TranslatableInterface $entity */ - $entity = $this->getEntity(); - $untranslated_entity = $entity->getUntranslated(); - // @todo Expand to work for all view modes in - // https://www.drupal.org/node/2907413. - $view_mode = 'full'; - // Retrieve the actual view mode from the returned view display as the - // requested view mode may not exist and a fallback will be used. - $view_mode = LayoutBuilderEntityViewDisplay::collectRenderDisplay($entity, $view_mode)->getMode(); - $contexts = [ - 'view_mode' => new Context(ContextDefinition::create('string'), $view_mode), - 'entity' => EntityContext::fromEntity($untranslated_entity), - 'display' => EntityContext::fromEntity(EntityViewDisplay::collectRenderDisplay($untranslated_entity, 'full')), - ]; - $label = new TranslatableMarkup('@entity being viewed', [ - '@entity' => $untranslated_entity->getEntityType()->getSingularLabel(), - ]); - $contexts['layout_builder.entity'] = EntityContext::fromEntity($untranslated_entity, $label); - - - $cacheability = new CacheableMetadata(); - return $this->sectionStorageManager->findByContext($contexts, $cacheability); - } + /** @var \Drupal\Core\Entity\TranslatableInterface $entity */ + $entity = $this->getEntity(); + $untranslated_entity = $entity->getUntranslated(); + return $untranslated_entity->get(static::FIELD_NAME)->getSections(); } - return NULL; + return []; } } diff --git a/core/modules/layout_builder/src/TranslatableOverridesSectionStorageInterface.php b/core/modules/layout_builder/src/TranslatableSectionStorageInterface.php similarity index 70% rename from core/modules/layout_builder/src/TranslatableOverridesSectionStorageInterface.php rename to core/modules/layout_builder/src/TranslatableSectionStorageInterface.php index d20cd7c042..d9326afe53 100644 --- a/core/modules/layout_builder/src/TranslatableOverridesSectionStorageInterface.php +++ b/core/modules/layout_builder/src/TranslatableSectionStorageInterface.php @@ -10,7 +10,7 @@ * experimental modules and development releases of contributed modules. * See https://www.drupal.org/core/experimental for more information. */ -interface TranslatableOverridesSectionStorageInterface extends OverridesSectionStorageInterface { +interface TranslatableSectionStorageInterface { /** * Indicates if the layout is translatable. @@ -29,11 +29,11 @@ public function isTranslatable(); public function isDefaultTranslation(); /** - * Gets the default translation section storage. + * Gets the layout default translation sections. * - * @return \Drupal\layout_builder\SectionStorageInterface - * The section storage used by the default translation. + * @return \Drupal\layout_builder\Section[] + * A sequentially and numerically keyed array of section objects. */ - public function getDefaultTranslationSectionStorage(); + public function getDefaultTranslationSections(); } diff --git a/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTranslationTest.php b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTranslationTest.php index ebf2c83e8f..841d4867ea 100644 --- a/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTranslationTest.php +++ b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTranslationTest.php @@ -18,6 +18,7 @@ class LayoutBuilderTranslationTest extends ContentTranslationTestBase { */ public static $modules = [ 'content_translation', + 'contextual', 'entity_test', 'layout_builder', 'block', @@ -57,9 +58,10 @@ public function testLayoutPerTranslation() { $assert_session->pageTextNotContains('The translated field value'); $assert_session->pageTextContains('The untranslated field value'); + // If there is not a layout override the layout translation is not + // accessible. $this->drupalGet($translated_layout_url); - $assert_session->pageTextNotContains('The untranslated field value'); - $assert_session->pageTextContains('The translated field value'); + $assert_session->pageTextContains('Access denied'); // Ensure that the tempstore varies per-translation. $this->drupalGet($layout_url); @@ -77,7 +79,7 @@ public function testLayoutPerTranslation() { // Confirm the tempstore for the translated layout is not affected. $this->drupalGet($translated_layout_url); - $assert_session->pageTextNotContains('Powered by Drupal'); + $assert_session->pageTextContains('Access denied'); $this->drupalGet($layout_url); $assert_session->pageTextContains('Powered by Drupal'); @@ -95,25 +97,25 @@ public function testLayoutPerTranslation() { $assert_session->pageTextContains('The translated field value'); $assert_session->pageTextContains('Powered by Drupal'); - // The translated entity's unaltered layout still persists in the tempstore. + // The translate layout is not available. $this->drupalGet($translated_layout_url); + $assert_session->pageTextNotContains('Access denied'); $assert_session->pageTextNotContains('The untranslated field value'); $assert_session->pageTextContains('The translated field value'); - $assert_session->pageTextNotContains('Powered by Drupal'); + $assert_session->pageTextContains('Powered by Drupal'); $assert_session->buttonExists('Save layout'); - $page->pressButton('Save layout'); - $assert_session->addressEquals($translated_entity_url); - $assert_session->pageTextNotContains('The untranslated field value'); - $assert_session->pageTextContains('The translated field value'); - $assert_session->pageTextNotContains('Powered by Drupal'); + // 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'); } /** - * Tests creating an override on a translation without an existing override. + * Tests that access is denied to a layout translation if there is override. */ - public function testLayoutTranslationFromDefault() { + public function testLayoutTranslationNoOverride() { $assert_session = $this->assertSession(); $page = $this->getSession()->getPage(); @@ -131,52 +133,10 @@ public function testLayoutTranslationFromDefault() { $assert_session->pageTextNotContains('The untranslated field value'); $assert_session->pageTextContains('The translated field value'); + // If there is not a layout override the layout translation is not + // accessible. $this->drupalGet($translated_layout_url); - $assert_session->pageTextNotContains('The untranslated field value'); - $assert_session->pageTextContains('The translated field value'); - - // Adjust the layout of the translated entity. - $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'); - - // Confirm the tempstore for the translated layout is not affected. - $this->drupalGet($layout_url); - $assert_session->pageTextNotContains('Powered by Drupal'); - - $this->drupalGet($translated_layout_url); - $assert_session->pageTextContains('Powered by Drupal'); - $assert_session->buttonExists('Save layout'); - $page->pressButton('Save layout'); - - // Ensure that the layout is on 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'); - - $this->drupalGet($entity_url); - $assert_session->pageTextNotContains('The translated field value'); - $assert_session->pageTextContains('The untranslated field value'); - $assert_session->pageTextNotContains('Powered by Drupal'); - - // The untranslated entity's unaltered layout still persists in the - // tempstore. - $this->drupalGet($layout_url); - $assert_session->pageTextContains('The untranslated field value'); - $assert_session->pageTextNotContains('The translated field value'); - $assert_session->pageTextNotContains('Powered by Drupal'); - $assert_session->buttonExists('Save layout'); - $page->pressButton('Save layout'); - - $assert_session->addressEquals($entity_url); - $assert_session->pageTextContains('The untranslated field value'); - $assert_session->pageTextNotContains('The translated field value'); - $assert_session->pageTextNotContains('Powered by Drupal'); + $assert_session->pageTextContains('Access denied'); } } diff --git a/core/modules/layout_builder/tests/src/Functional/UntranslatableLayoutTest.php b/core/modules/layout_builder/tests/src/Functional/UntranslatableLayoutTest.php deleted file mode 100644 index daf54b7de5..0000000000 --- a/core/modules/layout_builder/tests/src/Functional/UntranslatableLayoutTest.php +++ /dev/null @@ -1,70 +0,0 @@ -setUpViewDisplay(); - $this->setUpEntities(); - - $field_config = FieldConfig::loadByName($this->entityTypeId, $this->bundle, OverridesSectionStorage::FIELD_NAME); - $field_config->setTranslatable(FALSE); - $field_config->save(); - } - - /** - * Tests that layout translations are not available. - */ - public function testLayoutTranslationDenied() { - $assert_session = $this->assertSession(); - - $entity_url = $this->entity->toUrl('canonical')->toString(); - $language = \Drupal::languageManager()->getLanguage($this->langcodes[2]); - $translated_entity_url = $this->entity->toUrl('canonical', ['language' => $language])->toString(); - $layout_url = $entity_url . '/layout'; - $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($layout_url); - $assert_session->pageTextNotContains('Access denied'); - - $this->drupalGet($translated_layout_url); - $assert_session->pageTextContains('Access denied'); - } - -} diff --git a/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockTranslationTest.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockTranslationTest.php deleted file mode 100644 index d177753665..0000000000 --- a/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockTranslationTest.php +++ /dev/null @@ -1,253 +0,0 @@ -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 inline blocks works with content translation. - */ - public function testInlineBlockContentTranslation() { - $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->addInlineBlockToLayout('Block en', 'Block en body'); - $this->assertSaveLayout(); - $this->drupalGet('node/1'); - $assert_session->pageTextContains('Block en'); - $assert_session->pageTextContains('Block en body'); - - // 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 translate node's inline block. - $this->drupalGet('it/node/1/layout'); - $this->configureInlineBlock('Block en body', 'Block it body'); - $this->assertSaveLayout(); - - $this->drupalGet('node/1'); - $assert_session->pageTextContains('Block en body'); - $assert_session->pageTextNotContains('Block it body'); - - $this->drupalGet('it/node/1'); - $assert_session->pageTextContains('Block it body'); - $assert_session->pageTextNotContains('Block en body'); - - // Update the original node's inline block. - $this->drupalGet('node/1/layout'); - $this->configureInlineBlock('Block en body', 'Block en body updated'); - $this->assertSaveLayout(); - - $this->drupalGet('node/1'); - $assert_session->pageTextContains('Block en body updated'); - $assert_session->pageTextNotContains('Block it body'); - - $this->drupalGet('it/node/1'); - $assert_session->pageTextContains('Block it body'); - $assert_session->pageTextNotContains('Block en body updated'); - } - - /** - * Tests that an translated entity can override the layout from default. - */ - public function testInlineBlockContentTranslationOverrideFromDefault() { - $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' - ); - - // 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'); - - // Add an inline block to the default layout. - $this->drupalGet(static::FIELD_UI_PREFIX . '/display/default'); - $this->clickLink('Manage layout'); - $assert_session->addressEquals(static::FIELD_UI_PREFIX . '/display/default/layout'); - $this->addInlineBlockToLayout('Block title', 'The DEFAULT block body'); - $this->assertSaveLayout(); - - $this->drupalGet('node/1'); - $assert_session->pageTextContains('The DEFAULT block body'); - $this->drupalGet('it/node/1'); - $assert_session->pageTextContains('The DEFAULT block body'); - - // Override the translated node's layout. - $this->drupalGet('it/node/1/layout'); - $this->configureInlineBlock('The DEFAULT block body', 'Overridden block body'); - $this->assertSaveLayout(); - - $this->drupalGet('node/1'); - $assert_session->pageTextContains('The DEFAULT block body'); - $assert_session->pageTextNotContains('Overridden block body'); - $this->drupalGet('it/node/1'); - $assert_session->pageTextContains('Overridden block body'); - $assert_session->pageTextNotContains('The DEFAULT block body'); - } - - /** - * Tests deleting an translated entity with inline block. - */ - public function testDeletingTranslatedEntityWithInlineBlock() { - /** @var \Drupal\Core\Cron $cron */ - $cron = \Drupal::service('cron'); - /** @var \Drupal\layout_builder\InlineBlockUsage $usage */ - $usage = \Drupal::service('inline_block.usage'); - - $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', - 'delete 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' - ); - - // 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'); - - // Override the translated node's layout. - $this->drupalGet('it/node/1/layout'); - $this->addInlineBlockToLayout('Block it title', 'Block it body'); - $this->assertSaveLayout(); - $it_block = $this->getLatestBlockEntityId(); - $this->assertCount(1, $this->blockStorage->loadMultiple()); - - // Add an inline block to the original node. - $this->drupalGet('node/1/layout'); - $this->addInlineBlockToLayout('Block en title', 'Block en body'); - $this->assertSaveLayout(); - $this->assertCount(2, $this->blockStorage->loadMultiple()); - - // Remove the translation. - $delete_translation_url = Url::fromRoute('entity.node.content_translation_delete', [ - 'node' => 1, - 'language' => 'it', - ]); - $this->drupalGet($delete_translation_url); - $this->drupalPostForm(NULL, [], 'Delete Italian translation'); - - $cron->run(); - - $this->blockStorage->resetCache([$it_block]); - $this->assertEmpty($this->blockStorage->load($it_block)); - $this->assertCount(1, $this->blockStorage->loadMultiple()); - $this->assertEmpty($usage->getUsage($it_block)); - - $this->drupalGet('node/1'); - $assert_session->pageTextContains('Block en body'); - } - -}