diff --git a/core/modules/layout_builder/layout_builder.routing.yml b/core/modules/layout_builder/layout_builder.routing.yml index 0d24a894a9..e13f809de7 100644 --- a/core/modules/layout_builder/layout_builder.routing.yml +++ b/core/modules/layout_builder/layout_builder.routing.yml @@ -117,7 +117,6 @@ layout_builder.translate_block: _form: '\Drupal\layout_builder\Form\TranslateBlockForm' _title: 'Translate block' requirements: - _permission: 'configure any layout' _layout_builder_access: 'view' _layout_builder_translation_access: 'translated' options: @@ -132,7 +131,6 @@ layout_builder.translate_inline_block: _entity_form: 'block_content.layout_builder_translate' _title: 'Translate block' requirements: - _permission: 'configure any layout' _layout_builder_access: 'view' _layout_builder_translation_access: 'translated' options: diff --git a/core/modules/layout_builder/src/Access/LayoutBuilderTranslationAccessCheck.php b/core/modules/layout_builder/src/Access/LayoutBuilderTranslationAccessCheck.php index 9c1058d037..eaddc44e33 100644 --- a/core/modules/layout_builder/src/Access/LayoutBuilderTranslationAccessCheck.php +++ b/core/modules/layout_builder/src/Access/LayoutBuilderTranslationAccessCheck.php @@ -3,10 +3,9 @@ namespace Drupal\layout_builder\Access; use Drupal\Core\Access\AccessResult; -use Drupal\Core\Cache\RefinableCacheableDependencyInterface; use Drupal\Core\Routing\Access\AccessInterface; +use Drupal\layout_builder\LayoutEntityHelperTrait; use Drupal\layout_builder\SectionStorageInterface; -use Drupal\layout_builder\TranslatableSectionStorageInterface; use Symfony\Component\Routing\Route; /** @@ -19,6 +18,8 @@ */ class LayoutBuilderTranslationAccessCheck implements AccessInterface { + use LayoutEntityHelperTrait; + /** * Checks routing access to the default translation only layout. * @@ -32,17 +33,18 @@ class LayoutBuilderTranslationAccessCheck implements AccessInterface { */ 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()); - } - else { - throw new \UnexpectedValueException("Unexpected _layout_builder_translation_access route requirement: $translation_type"); - } - if ($access instanceof RefinableCacheableDependencyInterface) { - $access->addCacheableDependency($section_storage); + $is_translation = static::isTranslation($section_storage); + switch ($translation_type) { + case 'untranslated': + $access = AccessResult::allowedIf(!$is_translation); + break; + + case 'translated': + $access = AccessResult::allowedIf($is_translation); + break; + + default: + throw new \UnexpectedValueException("Unexpected _layout_builder_translation_access route requirement: $translation_type"); } return $access; } diff --git a/core/modules/layout_builder/src/Element/LayoutBuilder.php b/core/modules/layout_builder/src/Element/LayoutBuilder.php index 90b251e936..62bd6fce85 100644 --- a/core/modules/layout_builder/src/Element/LayoutBuilder.php +++ b/core/modules/layout_builder/src/Element/LayoutBuilder.php @@ -13,10 +13,10 @@ use Drupal\Core\Url; use Drupal\layout_builder\Context\LayoutBuilderContextTrait; use Drupal\layout_builder\LayoutBuilderHighlightTrait; +use Drupal\layout_builder\LayoutEntityHelperTrait; use Drupal\layout_builder\LayoutTempstoreRepositoryInterface; use Drupal\layout_builder\OverridesSectionStorageInterface; use Drupal\layout_builder\SectionStorageInterface; -use Drupal\layout_builder\TranslatableSectionStorageInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -32,6 +32,7 @@ class LayoutBuilder extends RenderElement implements ContainerFactoryPluginInter use AjaxHelperTrait; use LayoutBuilderContextTrait; use LayoutBuilderHighlightTrait; + use LayoutEntityHelperTrait; /** * The layout tempstore repository. @@ -49,6 +50,7 @@ class LayoutBuilder extends RenderElement implements ContainerFactoryPluginInter /** * The entity type manager. + * * @var \Drupal\Core\Entity\EntityTypeManagerInterface */ protected $entityTypeManager; @@ -67,12 +69,20 @@ class LayoutBuilder extends RenderElement implements ContainerFactoryPluginInter * @param \Drupal\Core\Messenger\MessengerInterface $messenger * The messenger service. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager - * The entity type manager. + * (optional) The entity type manager. + * + * @todo The current constructor signature is deprecated: + * - The $entity_type_manager parameter is optional but should become + * required. Deprecate in https://www.drupal.org/node/3058490. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, LayoutTempstoreRepositoryInterface $layout_tempstore_repository, MessengerInterface $messenger, EntityTypeManagerInterface $entity_type_manager) { + public function __construct(array $configuration, $plugin_id, $plugin_definition, LayoutTempstoreRepositoryInterface $layout_tempstore_repository, MessengerInterface $messenger, EntityTypeManagerInterface $entity_type_manager = NULL) { parent::__construct($configuration, $plugin_id, $plugin_definition); $this->layoutTempstoreRepository = $layout_tempstore_repository; $this->messenger = $messenger; + if ($entity_type_manager === NULL) { + @trigger_error('The entity_type.manager service must be passed to \Drupal\layout_builder\Element\LayoutBuilder::__construct(). It was added in Drupal 8.8.0 and will be required before Drupal 9.0.0.', E_USER_DEPRECATED); + $entity_type_manager = \Drupal::service('entity_type.manager'); + } $this->entityTypeManager = $entity_type_manager; } @@ -123,7 +133,7 @@ public function preRender($element) { */ protected function layout(SectionStorageInterface $section_storage) { $this->prepareLayout($section_storage); - $is_translation = $section_storage instanceof TranslatableSectionStorageInterface && !$section_storage->isDefaultTranslation(); + $is_translation = static::isTranslation($section_storage); $output = []; if ($this->isAjax()) { @@ -278,8 +288,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(); - $sections_editable = !$is_translation; + $sections_editable = !static::isTranslation($section_storage); $layout = $section->getLayout(); $build = $section->toRenderArray($this->getAvailableContexts($section_storage), TRUE); $layout_definition = $layout->getPluginDefinition(); @@ -294,50 +303,8 @@ protected function buildAdministrativeSection(SectionStorageInterface $section_s $build[$region][$uuid]['#attributes']['class'][] = 'layout-builder-block'; $build[$region][$uuid]['#attributes']['data-layout-block-uuid'] = $uuid; $build[$region][$uuid]['#attributes']['data-layout-builder-highlight-id'] = $this->blockUpdateHighlightId($uuid); - $contextual_link_settings = [ - 'route_parameters' => [ - 'section_storage_type' => $storage_type, - 'section_storage' => $storage_id, - 'delta' => $delta, - 'region' => $region, - 'uuid' => $uuid, - ], - ]; - if ($is_translation) { - $component = $section->getComponent($uuid); - if ($component->hasTranslatableConfiguration()) { - $contextual_group = 'layout_builder_block_translation'; - /** @var \Drupal\Core\Language\LanguageInterface $language */ - if ($language = $section_storage->getTranslationLanguage()) { - $contextual_link_settings['route_parameters']['langcode'] = $language->getId(); - } - - /** @var \Drupal\layout_builder\Plugin\Block\InlineBlock $plugin */ - $plugin = $component->getPlugin(); - if ($plugin instanceof DerivativeInspectionInterface && $plugin->getBaseId() === 'inline_block') { - $configuration = $plugin->getConfiguration(); - /** @var \Drupal\block_content\Entity\BlockContent $block */ - $block = $this->entityTypeManager->getStorage('block_content')->loadRevision($configuration['block_revision_id']); - if ($block->isTranslatable()) { - $contextual_group = 'layout_builder_inline_block_translation'; - } - } - } - } - else { - // Add metadata about the current operations available in - // contextual links. This will invalidate the client-side cache of - // links that were cached before the 'move' link was added. - // @see layout_builder.links.contextual.yml - $contextual_link_settings['metadata'] = [ - 'operations' => 'move:update:remove', - ]; - $contextual_group = 'layout_builder_block'; - } - if (isset($contextual_group)) { - $build[$region][$uuid]['#contextual_links'] = [ - $contextual_group => $contextual_link_settings, - ]; + if ($contextual_link_element = $this->createContextualLinkElement($section_storage, $delta, $region, $uuid)) { + $build[$region][$uuid]['#contextual_links'] = $contextual_link_element; } } } @@ -470,4 +437,75 @@ protected function buildAdministrativeSection(SectionStorageInterface $section_s ]; } + /** + * Creates contextual link element for a component. + * + * @param \Drupal\layout_builder\SectionStorageInterface $section_storage + * The section storage. + * @param $delta + * The section delta. + * @param $region + * The region. + * @param $uuid + * The UUID of the component. + * @param $is_translation + * Whether the section storage is handling a translation. + * + * @return array|null + * The contextual link render array or NULL if none. + * + */ + protected function createContextualLinkElement(SectionStorageInterface $section_storage, $delta, $region, $uuid) { + $contextual_link_element = NULL; + $section = $section_storage->getSection($delta); + $contextual_link_settings = [ + 'route_parameters' => [ + 'section_storage_type' => $section_storage->getStorageType(), + 'section_storage' => $section_storage->getStorageId(), + 'delta' => $delta, + 'region' => $region, + 'uuid' => $uuid, + ], + ]; + if (static::isTranslation($section_storage)) { + $component = $section->getComponent($uuid); + if ($component->hasTranslatableConfiguration()) { + $contextual_group = 'layout_builder_block_translation'; + /** @var \Drupal\Core\Language\LanguageInterface $language */ + if ($language = $section_storage->getTranslationLanguage()) { + $contextual_link_settings['route_parameters']['langcode'] = $language->getId(); + } + + /** @var \Drupal\layout_builder\Plugin\Block\InlineBlock $plugin */ + $plugin = $component->getPlugin(); + if ($plugin instanceof DerivativeInspectionInterface && $plugin->getBaseId() === 'inline_block') { + $configuration = $plugin->getConfiguration(); + /** @var \Drupal\block_content\Entity\BlockContent $block */ + $block = $this->entityTypeManager->getStorage('block_content') + ->loadRevision($configuration['block_revision_id']); + if ($block->isTranslatable()) { + $contextual_group = 'layout_builder_inline_block_translation'; + } + } + } + } + else { + // Add metadata about the current operations available in + // contextual links. This will invalidate the client-side cache of + // links that were cached before the 'move' link was added. + // @see layout_builder.links.contextual.yml + $contextual_link_settings['metadata'] = [ + 'operations' => 'move:update:remove', + ]; + $contextual_group = 'layout_builder_block'; + } + if (isset($contextual_group)) { + $contextual_link_element = [ + $contextual_group => $contextual_link_settings, + ]; + + } + return $contextual_link_element; + } + } diff --git a/core/modules/layout_builder/src/EventSubscriber/ComponentPluginTranslate.php b/core/modules/layout_builder/src/EventSubscriber/ComponentPluginTranslate.php index 6ac6d74eb0..9007945d74 100644 --- a/core/modules/layout_builder/src/EventSubscriber/ComponentPluginTranslate.php +++ b/core/modules/layout_builder/src/EventSubscriber/ComponentPluginTranslate.php @@ -8,7 +8,6 @@ use Drupal\layout_builder\Event\SectionComponentBuildRenderArrayEvent; use Drupal\layout_builder\LayoutBuilderEvents; use Drupal\layout_builder\LayoutEntityHelperTrait; -use Drupal\layout_builder\TranslatableSectionStorageInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; /** @@ -29,6 +28,8 @@ class ComponentPluginTranslate implements EventSubscriberInterface { protected $languageManager; /** + * The current route match. + * * @var \Drupal\Core\Routing\RouteMatchInterface */ protected $routeMatch; @@ -70,6 +71,7 @@ public function onBuildRender(SectionComponentBuildRenderArrayEvent $event) { return; } + // @todo Change to 'entity' in https://www.drupal.org/node/3018782. $entity = $contexts['layout_builder.entity']->getContextValue(); $configuration = $plugin->getConfiguration(); if ($event->inPreview()) { @@ -79,7 +81,7 @@ public function onBuildRender(SectionComponentBuildRenderArrayEvent $event) { $section_storage = $this->getSectionStorageForEntity($entity); } - if ($section_storage instanceof TranslatableSectionStorageInterface && !$section_storage->isDefaultTranslation()) { + if (static::isTranslation($section_storage)) { if ($translated_plugin_configuration = $section_storage->getTranslatedComponentConfiguration($component->getUuid())) { $translated_plugin_configuration += $configuration; $plugin->setConfiguration($translated_plugin_configuration); diff --git a/core/modules/layout_builder/src/Form/BlockContentInlineBlockTranslateForm.php b/core/modules/layout_builder/src/Form/BlockContentInlineBlockTranslateForm.php index f89f69e051..a7b1e59798 100644 --- a/core/modules/layout_builder/src/Form/BlockContentInlineBlockTranslateForm.php +++ b/core/modules/layout_builder/src/Form/BlockContentInlineBlockTranslateForm.php @@ -131,8 +131,10 @@ public function save(array $form, FormStateInterface $form_state) { */ public function form(array $form, FormStateInterface $form_state) { $form = parent::form($form, $form_state); + // The language of the translation cannot be changed. $form['langcode']['#access'] = FALSE; $form['revision_log']['#access'] = FALSE; + // Creating new revisions is based on the entity with the layout. $form['revision']['#access'] = FALSE; return $form; } diff --git a/core/modules/layout_builder/src/Form/BlockPluginTranslationForm.php b/core/modules/layout_builder/src/Form/BlockPluginTranslationForm.php index 3d525ffcbd..9224bcaf8c 100644 --- a/core/modules/layout_builder/src/Form/BlockPluginTranslationForm.php +++ b/core/modules/layout_builder/src/Form/BlockPluginTranslationForm.php @@ -2,14 +2,11 @@ namespace Drupal\layout_builder\Form; -use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Plugin\ContextAwarePluginAssignmentTrait; use Drupal\Core\Plugin\PluginFormBase; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\layout_builder\LayoutBuilderPluginTranslationFormInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; /** * Provides a block plugin form for translatable settings in the Layout Builder. @@ -55,6 +52,9 @@ public function validateConfigurationForm(array &$form, FormStateInterface $form /** * {@inheritdoc} + * + * We only saving the label translation the label the form values will be + * saved in \Drupal\layout_builder\Form\TranslateBlockForm::submitForm(). */ public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { } diff --git a/core/modules/layout_builder/src/Form/OverridesEntityForm.php b/core/modules/layout_builder/src/Form/OverridesEntityForm.php index a3988f27a0..47d7dcd117 100644 --- a/core/modules/layout_builder/src/Form/OverridesEntityForm.php +++ b/core/modules/layout_builder/src/Form/OverridesEntityForm.php @@ -9,11 +9,11 @@ use Drupal\Core\Entity\EntityRepositoryInterface; use Drupal\Core\Entity\EntityTypeBundleInfoInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\layout_builder\LayoutEntityHelperTrait; use Drupal\layout_builder\LayoutTempstoreRepositoryInterface; use Drupal\layout_builder\OverridesSectionStorageInterface; use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage; use Drupal\layout_builder\SectionStorageInterface; -use Drupal\layout_builder\TranslatableSectionStorageInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -25,6 +25,7 @@ class OverridesEntityForm extends ContentEntityForm { use PreviewToggleTrait; + use LayoutEntityHelperTrait; /** * Layout tempstore repository. @@ -83,7 +84,7 @@ protected function init(FormStateInterface $form_state) { parent::init($form_state); $form_display = EntityFormDisplay::collectRenderDisplay($this->entity, $this->getOperation(), FALSE); - $field_name = $this->sectionStorage instanceof TranslatableSectionStorageInterface && !$this->sectionStorage->isDefaultTranslation() ? + $field_name = static::isTranslation($this->sectionStorage) ? OverridesSectionStorage::TRANSLATED_CONFIGURATION_FIELD_NAME : OverridesSectionStorage::FIELD_NAME; $form_display->setComponent($field_name, [ @@ -202,7 +203,7 @@ protected function actions(array $form, FormStateInterface $form_state) { '#submit' => ['::redirectOnSubmit'], '#redirect' => 'discard_changes', ]; - if (!$this->sectionStorage instanceof TranslatableSectionStorageInterface || $this->sectionStorage->isDefaultTranslation()) { + if (!static::isTranslation($this->sectionStorage)) { // @todo This button should be conditionally displayed, see // https://www.drupal.org/node/2917777. $actions['revert'] = [ diff --git a/core/modules/layout_builder/src/InlineBlockEntityOperations.php b/core/modules/layout_builder/src/InlineBlockEntityOperations.php index 89a3b41b99..e0d0fe6955 100644 --- a/core/modules/layout_builder/src/InlineBlockEntityOperations.php +++ b/core/modules/layout_builder/src/InlineBlockEntityOperations.php @@ -208,7 +208,7 @@ public function handlePreSave(EntityInterface $entity) { $section_storage = $this->getSectionStorageForEntity($entity); foreach ($this->getInlineBlockComponents($sections) as $component) { - if ($section_storage instanceof TranslatableSectionStorageInterface && !$section_storage->isDefaultTranslation()) { + if (static::isTranslation($section_storage)) { $translated_component_configuration = $section_storage->getTranslatedComponentConfiguration($component->getUuid()); if (isset($translated_component_configuration['block_serialized'])) { $this->saveTranslatedInlineBlock($entity, $component->getUuid(), $translated_component_configuration, $new_revision); @@ -321,15 +321,22 @@ protected function saveInlineBlockComponent(EntityInterface $entity, SectionComp * @param bool $new_revision * Whether a new revision of the block should be created. */ - protected function saveTranslatedInlineBlock($entity, $component_uuid, $translated_component_configuration, $new_revision) { + protected function saveTranslatedInlineBlock(EntityInterface $entity, $component_uuid, array $translated_component_configuration, $new_revision) { /** @var \Drupal\block_content\BlockContentInterface $block */ $block = unserialize($translated_component_configuration['block_serialized']); + // Create a InlineBlock plugin from the translated configuration in order to + // save the block. /** @var \Drupal\layout_builder\Plugin\Block\InlineBlock $plugin */ $plugin = $this->blockManager->createInstance('inline_block:' . $block->bundle(), $translated_component_configuration); $plugin->saveBlockContent($new_revision); - $configuration = $plugin->getConfiguration(); + // Remove serialized block after the block has been saved. unset($translated_component_configuration['block_serialized']); + + // Update the block_revision_id in the translated configuration which may + // have changed after saving the block. + $configuration = $plugin->getConfiguration(); $translated_component_configuration['block_revision_id'] = $configuration['block_revision_id']; + /** @var \Drupal\layout_builder\TranslatableSectionStorageInterface $section_storage */ $section_storage = $this->getSectionStorageForEntity($entity); $section_storage->setTranslatedComponentConfiguration($component_uuid, $translated_component_configuration); diff --git a/core/modules/layout_builder/src/LayoutEntityHelperTrait.php b/core/modules/layout_builder/src/LayoutEntityHelperTrait.php index e1c4216ce7..95d9f0aa06 100644 --- a/core/modules/layout_builder/src/LayoutEntityHelperTrait.php +++ b/core/modules/layout_builder/src/LayoutEntityHelperTrait.php @@ -177,4 +177,17 @@ private function sectionStorageManager() { return $this->sectionStorageManager ?: \Drupal::service('plugin.manager.layout_builder.section_storage'); } + /** + * Determines if the sections is for a translation. + * + * @param \Drupal\layout_builder\SectionStorageInterface $section_storage + * The section storage. + * + * @return bool + * TRUE if the section storage is for translation otherwise false. + */ + protected static function isTranslation(SectionStorageInterface $section_storage) { + return $section_storage instanceof TranslatableSectionStorageInterface && !$section_storage->isDefaultTranslation(); + } + } diff --git a/core/modules/layout_builder/src/Plugin/Block/InlineBlock.php b/core/modules/layout_builder/src/Plugin/Block/InlineBlock.php index 169dd3e1d4..c3b4b6409f 100644 --- a/core/modules/layout_builder/src/Plugin/Block/InlineBlock.php +++ b/core/modules/layout_builder/src/Plugin/Block/InlineBlock.php @@ -326,7 +326,7 @@ public function saveBlockContent($new_revision = FALSE, $duplicate_block = FALSE */ public function hasTranslatableConfiguration() { $block_content = $this->getEntity(); - return $block_content->isTranslatable() || (!empty($this->configuration['label_display']) && !empty($this->configuration['label'])); + return $block_content->isTranslatable() || (!empty($this->configuration['label_display']) && !empty($this->configuration['label'])); } } diff --git a/core/modules/layout_builder/src/Plugin/Field/FieldType/LayoutTranslationItem.php b/core/modules/layout_builder/src/Plugin/Field/FieldType/LayoutTranslationItem.php index 7491e8a46e..2d178ed4a7 100644 --- a/core/modules/layout_builder/src/Plugin/Field/FieldType/LayoutTranslationItem.php +++ b/core/modules/layout_builder/src/Plugin/Field/FieldType/LayoutTranslationItem.php @@ -34,19 +34,6 @@ public static function propertyDefinitions(FieldStorageDefinitionInterface $fiel return $properties; } - /** - * {@inheritdoc} - */ - public function __get($name) { - // @todo \Drupal\Core\Field\FieldItemBase::__get() does not return default - // values for uninstantiated properties. This will forcibly instantiate - // all properties with the side-effect of a performance hit, resolve - // properly in https://www.drupal.org/node/2413471. - $this->getProperties(); - - return parent::__get($name); - } - /** * {@inheritdoc} */ diff --git a/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php index 9358c4fcbd..cc86df0c48 100644 --- a/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php @@ -490,9 +490,7 @@ public function getTranslatedComponentConfiguration($uuid) { if ($this->getEntity()->get(OverridesSectionStorage::TRANSLATED_CONFIGURATION_FIELD_NAME)->isEmpty()) { return []; } - else { - $translation_settings = $this->getEntity()->get(OverridesSectionStorage::TRANSLATED_CONFIGURATION_FIELD_NAME)->getValue()[0]; - } + $translation_settings = $this->getEntity()->get(OverridesSectionStorage::TRANSLATED_CONFIGURATION_FIELD_NAME)->getValue()[0]; return isset($translation_settings['value']['components'][$uuid]) ? $translation_settings['value']['components'][$uuid] : []; }