diff --git a/core/modules/hal/tests/src/Functional/quickedit/QuickEditLayoutBuilderEntityViewDisplayHalJsonAnonTest.php b/core/modules/hal/tests/src/Functional/quickedit/QuickEditLayoutBuilderEntityViewDisplayHalJsonAnonTest.php new file mode 100644 index 0000000000000000000000000000000000000000..128c50ebe0801c8553a2e824e13625422d59b6fb --- /dev/null +++ b/core/modules/hal/tests/src/Functional/quickedit/QuickEditLayoutBuilderEntityViewDisplayHalJsonAnonTest.php @@ -0,0 +1,18 @@ +<?php + +namespace Drupal\Tests\hal\Functional\quickedit; + +use Drupal\Tests\hal\Functional\layout_builder\LayoutBuilderEntityViewDisplayHalJsonAnonTest; + +/** + * @group hal + * @group legacy + */ +class QuickEditLayoutBuilderEntityViewDisplayHalJsonAnonTest extends LayoutBuilderEntityViewDisplayHalJsonAnonTest { + + /** + * {@inheritdoc} + */ + protected static $modules = ['quickedit']; + +} diff --git a/core/modules/hal/tests/src/Functional/quickedit/QuickEditLayoutBuilderEntityViewDisplayHalJsonBasicAuthTest.php b/core/modules/hal/tests/src/Functional/quickedit/QuickEditLayoutBuilderEntityViewDisplayHalJsonBasicAuthTest.php new file mode 100644 index 0000000000000000000000000000000000000000..7ec69ae947ba9ad841df96c29c1d98877d8dceec --- /dev/null +++ b/core/modules/hal/tests/src/Functional/quickedit/QuickEditLayoutBuilderEntityViewDisplayHalJsonBasicAuthTest.php @@ -0,0 +1,18 @@ +<?php + +namespace Drupal\Tests\hal\Functional\quickedit; + +use Drupal\Tests\hal\Functional\layout_builder\LayoutBuilderEntityViewDisplayHalJsonBasicAuthTest; + +/** + * @group hal + * @group legacy + */ +class QuickEditLayoutBuilderEntityViewDisplayHalJsonBasicAuthTest extends LayoutBuilderEntityViewDisplayHalJsonBasicAuthTest { + + /** + * {@inheritdoc} + */ + protected static $modules = ['quickedit']; + +} diff --git a/core/modules/hal/tests/src/Functional/quickedit/QuickEditLayoutBuilderEntityViewDisplayHalJsonCookieTest.php b/core/modules/hal/tests/src/Functional/quickedit/QuickEditLayoutBuilderEntityViewDisplayHalJsonCookieTest.php new file mode 100644 index 0000000000000000000000000000000000000000..8e9520663a1c952c189177fa7401d3315978ad28 --- /dev/null +++ b/core/modules/hal/tests/src/Functional/quickedit/QuickEditLayoutBuilderEntityViewDisplayHalJsonCookieTest.php @@ -0,0 +1,18 @@ +<?php + +namespace Drupal\Tests\hal\Functional\quickedit; + +use Drupal\Tests\hal\Functional\layout_builder\LayoutBuilderEntityViewDisplayHalJsonCookieTest; + +/** + * @group hal + * @group legacy + */ +class QuickEditLayoutBuilderEntityViewDisplayHalJsonCookieTest extends LayoutBuilderEntityViewDisplayHalJsonCookieTest { + + /** + * {@inheritdoc} + */ + protected static $modules = ['quickedit']; + +} diff --git a/core/modules/jsonapi/src/Normalizer/ResourceObjectNormalizer.php b/core/modules/jsonapi/src/Normalizer/ResourceObjectNormalizer.php index e4988c9096e98ed40d79f30d0db0ba0be187690e..340fcb21bd92f1663bd8565da249d98324825ca9 100644 --- a/core/modules/jsonapi/src/Normalizer/ResourceObjectNormalizer.php +++ b/core/modules/jsonapi/src/Normalizer/ResourceObjectNormalizer.php @@ -192,7 +192,7 @@ protected function serializeField($field, array $context, $format) { // @todo Replace this workaround after https://www.drupal.org/node/3043245 // or remove the need for this in https://www.drupal.org/node/2942975. // See \Drupal\layout_builder\Normalizer\LayoutEntityDisplayNormalizer. - if ($context['resource_object']->getResourceType()->getDeserializationTargetClass() === 'Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay' && $context['resource_object']->getField('third_party_settings') === $field) { + if (is_a($context['resource_object']->getResourceType()->getDeserializationTargetClass(), 'Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay', TRUE) && $context['resource_object']->getField('third_party_settings') === $field) { unset($field['layout_builder']['sections']); } diff --git a/core/modules/layout_builder/layout_builder.module b/core/modules/layout_builder/layout_builder.module index 6e8d0a25f93a20a25303b4cfbb3f6f1f63c55a8f..1646139aa7bf8aed43d3d91c698a6311746a7215 100644 --- a/core/modules/layout_builder/layout_builder.module +++ b/core/modules/layout_builder/layout_builder.module @@ -26,7 +26,6 @@ use Drupal\Core\Session\AccountInterface; use Drupal\Core\Access\AccessResult; use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage; -use Drupal\layout_builder\QuickEditIntegration; /** * Implements hook_help(). @@ -162,12 +161,6 @@ function layout_builder_entity_view_alter(array &$build, EntityInterface $entity if ($display instanceof LayoutBuilderEntityViewDisplay && strpos($route_name, 'layout_builder.') === 0) { unset($build['#contextual_links']); } - - if (\Drupal::moduleHandler()->moduleExists('quickedit')) { - /** @var \Drupal\layout_builder\QuickEditIntegration $quick_edit_integration */ - $quick_edit_integration = \Drupal::classResolver(QuickEditIntegration::class); - $quick_edit_integration->entityViewAlter($build, $entity, $display); - } } /** @@ -351,15 +344,6 @@ function layout_builder_system_breadcrumb_alter(Breadcrumb &$breadcrumb, RouteMa } } -/** - * Implements hook_quickedit_render_field(). - */ -function layout_builder_quickedit_render_field(EntityInterface $entity, $field_name, $view_mode_id, $langcode) { - /** @var \Drupal\layout_builder\QuickEditIntegration $quick_edit_integration */ - $quick_edit_integration = \Drupal::classResolver(QuickEditIntegration::class); - return $quick_edit_integration->quickEditRenderField($entity, $field_name, $view_mode_id, $langcode); -} - /** * Implements hook_entity_translation_create(). */ diff --git a/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php index d543fe1619557edbaf532c41c700706515218e93..c63b96ff01d50d12991ae7f893fbd07436fed4fc 100644 --- a/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php +++ b/core/modules/layout_builder/src/Entity/LayoutBuilderEntityViewDisplay.php @@ -18,7 +18,6 @@ use Drupal\field\Entity\FieldStorageConfig; use Drupal\layout_builder\LayoutEntityHelperTrait; use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage; -use Drupal\layout_builder\QuickEditIntegration; use Drupal\layout_builder\Section; use Drupal\layout_builder\SectionComponent; use Drupal\layout_builder\SectionListTrait; @@ -473,7 +472,7 @@ private function sectionStorageManager() { * {@inheritdoc} */ public function getComponent($name) { - if ($this->isLayoutBuilderEnabled() && $section_component = $this->getQuickEditSectionComponent() ?: $this->getSectionComponentForFieldName($name)) { + if ($this->isLayoutBuilderEnabled() && $section_component = $this->getSectionComponentForFieldName($name)) { $plugin = $section_component->getPlugin(); if ($plugin instanceof ConfigurableInterface) { $configuration = $plugin->getConfiguration(); @@ -485,43 +484,6 @@ public function getComponent($name) { return parent::getComponent($name); } - /** - * Returns the Quick Edit formatter settings. - * - * @return \Drupal\layout_builder\SectionComponent|null - * The section component if it is available. - * - * @see \Drupal\layout_builder\QuickEditIntegration::entityViewAlter() - * @see \Drupal\quickedit\MetadataGenerator::generateFieldMetadata() - */ - private function getQuickEditSectionComponent() { - // To determine the Quick Edit view_mode ID we need an originalMode set. - if ($original_mode = $this->getOriginalMode()) { - $parts = explode('-', $original_mode); - // The Quick Edit view mode ID is created by - // \Drupal\layout_builder\QuickEditIntegration::entityViewAlter() - // concatenating together the information we need to retrieve the Layout - // Builder component. It follows the structure prescribed by the - // documentation of hook_quickedit_render_field(). - if (count($parts) === 6 && $parts[0] === 'layout_builder') { - [, $delta, $component_uuid, $entity_id] = QuickEditIntegration::deconstructViewModeId($original_mode); - $entity = $this->entityTypeManager()->getStorage($this->getTargetEntityTypeId())->load($entity_id); - $sections = $this->getEntitySections($entity); - if (isset($sections[$delta])) { - $component = $sections[$delta]->getComponent($component_uuid); - $plugin = $component->getPlugin(); - // We only care about FieldBlock because these are only components - // that provide Quick Edit integration: Quick Edit enables in-place - // editing of fields of entities, not of anything else. - if ($plugin instanceof DerivativeInspectionInterface && $plugin->getBaseId() === 'field_block') { - return $component; - } - } - } - } - return NULL; - } - /** * Gets the component for a given field name if any. * diff --git a/core/modules/layout_builder/src/QuickEditIntegration.php b/core/modules/layout_builder/src/QuickEditIntegration.php index c087f90971f5e7759bd0ff91c1a10317685391f3..7e2a5c4e31ae4d4b4e010a2ace6a12e6cada52f9 100644 --- a/core/modules/layout_builder/src/QuickEditIntegration.php +++ b/core/modules/layout_builder/src/QuickEditIntegration.php @@ -2,21 +2,9 @@ namespace Drupal\layout_builder; -use Drupal\Component\Utility\NestedArray; -use Drupal\Core\Cache\CacheableMetadata; -use Drupal\Core\DependencyInjection\ContainerInjectionInterface; -use Drupal\Core\Entity\Display\EntityViewDisplayInterface; -use Drupal\Core\Entity\EntityInterface; -use Drupal\Core\Entity\EntityTypeManagerInterface; -use Drupal\Core\Entity\FieldableEntityInterface; -use Drupal\Core\Logger\LoggerChannelTrait; -use Drupal\Core\Plugin\Context\Context; -use Drupal\Core\Plugin\Context\ContextDefinition; -use Drupal\Core\Plugin\Context\EntityContext; -use Drupal\Core\Render\Element; -use Drupal\Core\Session\AccountInterface; -use Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; +@trigger_error(__NAMESPACE__ . '\QuickEditIntegration is deprecated in drupal:9.5.0 and is removed from drupal:10.0.0. Instead, use \Drupal\quickedit\LayoutBuilderIntegration. See https://www.drupal.org/node/3265518', E_USER_DEPRECATED); + +use Drupal\quickedit\LayoutBuilderIntegration; /** * Helper methods for Quick Edit module integration. @@ -24,298 +12,4 @@ * @internal * This is an internal utility class wrapping hook implementations. */ -class QuickEditIntegration implements ContainerInjectionInterface { - - use LoggerChannelTrait; - - /** - * The section storage manager. - * - * @var \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface - */ - protected $sectionStorageManager; - - /** - * The current user. - * - * @var \Drupal\Core\Session\AccountInterface - */ - protected $currentUser; - - /** - * The entity type manager. - * - * @var \Drupal\Core\Entity\EntityTypeManagerInterface - */ - protected $entityTypeManager; - - /** - * Constructs a new QuickEditIntegration object. - * - * @param \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface $section_storage_manager - * The section storage manager. - * @param \Drupal\Core\Session\AccountInterface $current_user - * The current user. - * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager - * The entity type manager. - */ - public function __construct(SectionStorageManagerInterface $section_storage_manager, AccountInterface $current_user, EntityTypeManagerInterface $entity_type_manager) { - $this->sectionStorageManager = $section_storage_manager; - $this->currentUser = $current_user; - $this->entityTypeManager = $entity_type_manager; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container) { - return new static( - $container->get('plugin.manager.layout_builder.section_storage'), - $container->get('current_user'), - $container->get('entity_type.manager') - ); - } - - /** - * Alters the entity view build for Quick Edit compatibility. - * - * When rendering fields outside of normal view modes, Quick Edit requires - * that modules identify themselves with a view mode ID in the format - * [module_name]-[information the module needs to rerender], as prescribed by - * hook_quickedit_render_field(). - * - * @param array $build - * The built entity render array. - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity. - * @param \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display - * The entity view display. - * - * @see hook_quickedit_render_field() - * @see layout_builder_quickedit_render_field() - */ - public function entityViewAlter(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display) { - if (!$entity instanceof FieldableEntityInterface || !isset($build['_layout_builder'])) { - return; - } - - $build['#cache']['contexts'][] = 'user.permissions'; - if (!$this->currentUser->hasPermission('access in-place editing')) { - return; - } - - $cacheable_metadata = CacheableMetadata::createFromRenderArray($build); - $section_list = $this->sectionStorageManager->findByContext( - [ - 'display' => EntityContext::fromEntity($display), - 'entity' => EntityContext::fromEntity($entity), - 'view_mode' => new Context(new ContextDefinition('string'), $display->getMode()), - ], - $cacheable_metadata - ); - $cacheable_metadata->applyTo($build); - - if (empty($section_list)) { - return; - } - - // Create a hash of the sections and use it in the unique Quick Edit view - // mode ID. Any changes to the sections will result in a different hash, - // forcing Quick Edit's JavaScript to recognize any changes and retrieve - // up-to-date metadata. - $sections_hash = hash('sha256', serialize($section_list->getSections())); - - // Track each component by their plugin ID, delta, region, and UUID. - $plugin_ids_to_update = []; - foreach (Element::children($build['_layout_builder']) as $delta) { - $section = $build['_layout_builder'][$delta]; - - if (!Element::isEmpty($section)) { - /** @var \Drupal\Core\Layout\LayoutDefinition $layout */ - $layout = $section['#layout']; - $regions = $layout->getRegionNames(); - - foreach ($regions as $region) { - if (isset($section[$region])) { - foreach ($section[$region] as $uuid => $component) { - if (isset($component['#plugin_id']) && $this->supportQuickEditOnComponent($component, $entity)) { - $plugin_ids_to_update[$component['#plugin_id']][$delta][$region][$uuid] = $uuid; - } - } - } - } - } - } - - // @todo Remove when https://www.drupal.org/node/3041850 is resolved. - $plugin_ids_to_update = array_filter($plugin_ids_to_update, function ($info) { - // Delta, region, and UUID each count as one. - return count($info, COUNT_RECURSIVE) === 3; - }); - - $plugin_ids_to_update = NestedArray::mergeDeepArray($plugin_ids_to_update, TRUE); - foreach ($plugin_ids_to_update as $delta => $regions) { - foreach ($regions as $region => $uuids) { - foreach ($uuids as $uuid => $component) { - $build['_layout_builder'][$delta][$region][$uuid]['content']['#view_mode'] = static::getViewModeId($entity, $display, $delta, $uuid, $sections_hash); - } - } - } - // Alter the Quick Edit view mode ID of all fields outside of the Layout - // Builder sections to force Quick Edit to request to the field metadata. - // @todo Remove this logic in https://www.drupal.org/project/node/2966136. - foreach (Element::children($build) as $field_name) { - if ($field_name !== '_layout_builder') { - $field_build = &$build[$field_name]; - if (isset($field_build['#view_mode'])) { - $field_build['#view_mode'] = "layout_builder-{$display->getMode()}-non_component-$sections_hash"; - } - } - } - } - - /** - * Generates a Quick Edit view mode ID. - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity. - * @param \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display - * The entity view display. - * @param int $delta - * The delta. - * @param string $component_uuid - * The component UUID. - * @param string $sections_hash - * The hash of the sections; must change whenever the sections change. - * - * @return string - * The Quick Edit view mode ID. - * - * @see \Drupal\layout_builder\QuickEditIntegration::deconstructViewModeId() - */ - private static function getViewModeId(EntityInterface $entity, EntityViewDisplayInterface $display, $delta, $component_uuid, $sections_hash) { - return implode('-', [ - 'layout_builder', - $display->getMode(), - $delta, - // Replace the dashes in the component UUID because we need to - // use dashes to join the parts. - str_replace('-', '_', $component_uuid), - $entity->id(), - $sections_hash, - ]); - } - - /** - * Deconstructs the Quick Edit view mode ID into its constituent parts. - * - * @param string $quick_edit_view_mode_id - * The Quick Edit view mode ID. - * - * @return array - * An array containing the entity view mode ID, the delta, the component - * UUID, and the entity ID. - * - * @see \Drupal\layout_builder\QuickEditIntegration::getViewModeId() - */ - public static function deconstructViewModeId($quick_edit_view_mode_id) { - [, $entity_view_mode_id, $delta, $component_uuid, $entity_id] = explode('-', $quick_edit_view_mode_id, 7); - return [ - $entity_view_mode_id, - // @todo Explicitly cast delta to an integer, remove this in - // https://www.drupal.org/project/drupal/issues/2984509. - (int) $delta, - // Replace the underscores with dash to get back the component UUID. - str_replace('_', '-', $component_uuid), - $entity_id, - ]; - } - - /** - * Re-renders a field rendered by Layout Builder, edited with Quick Edit. - * - * @param \Drupal\Core\Entity\FieldableEntityInterface $entity - * The entity. - * @param string $field_name - * The field name. - * @param string $quick_edit_view_mode_id - * The Quick Edit view mode ID. - * @param string $langcode - * The language code. - * - * @return array - * The re-rendered field. - */ - public function quickEditRenderField(FieldableEntityInterface $entity, $field_name, $quick_edit_view_mode_id, $langcode) { - [$entity_view_mode, $delta, $component_uuid] = static::deconstructViewModeId($quick_edit_view_mode_id); - - $entity_build = $this->entityTypeManager->getViewBuilder($entity->getEntityTypeId())->view($entity, $entity_view_mode, $langcode); - $this->buildEntityView($entity_build); - - if (isset($entity_build['_layout_builder'][$delta])) { - foreach (Element::children($entity_build['_layout_builder'][$delta]) as $region) { - if (isset($entity_build['_layout_builder'][$delta][$region][$component_uuid])) { - return $entity_build['_layout_builder'][$delta][$region][$component_uuid]['content']; - } - } - } - - $this->getLogger('layout_builder')->warning('The field "%field" failed to render.', ['%field' => $field_name]); - return []; - } - - /** - * {@inheritdoc} - * - * @todo Replace this hardcoded processing when - * https://www.drupal.org/project/drupal/issues/3041635 is resolved. - * - * @see \Drupal\Tests\EntityViewTrait::buildEntityView() - */ - private function buildEntityView(array &$elements) { - // If the default values for this element have not been loaded yet, - // populate them. - if (isset($elements['#type']) && empty($elements['#defaults_loaded'])) { - $elements += \Drupal::service('element_info')->getInfo($elements['#type']); - } - - // Make any final changes to the element before it is rendered. This means - // that the $element or the children can be altered or corrected before - // the element is rendered into the final text. - if (isset($elements['#pre_render'])) { - foreach ($elements['#pre_render'] as $callable) { - $elements = call_user_func($callable, $elements); - } - } - - // And recurse. - $children = Element::children($elements, TRUE); - foreach ($children as $key) { - $this->buildEntityView($elements[$key]); - } - } - - /** - * Determines whether a component has Quick Edit support. - * - * Only field_block components for display configurable fields should be - * supported. - * - * @param array $component - * The component render array. - * @param \Drupal\Core\Entity\FieldableEntityInterface $entity - * The entity being displayed. - * - * @return bool - * Whether Quick Edit is supported on the component. - * - * @see \Drupal\layout_builder\Plugin\Block\FieldBlock - */ - private function supportQuickEditOnComponent(array $component, FieldableEntityInterface $entity) { - if (isset($component['content']['#field_name'], $component['#base_plugin_id']) && $component['#base_plugin_id'] === 'field_block' && $entity->hasField($component['content']['#field_name'])) { - return $entity->getFieldDefinition($component['content']['#field_name'])->isDisplayConfigurable('view'); - } - return FALSE; - } - -} +class QuickEditIntegration extends LayoutBuilderIntegration {} diff --git a/core/modules/quickedit/quickedit.module b/core/modules/quickedit/quickedit.module index 028fe20143516ecaf3741e6e330fa5fe03d42f1d..219b975ebcbf2e8eaa96ed4526e78cfc8b58887c 100644 --- a/core/modules/quickedit/quickedit.module +++ b/core/modules/quickedit/quickedit.module @@ -16,6 +16,8 @@ use Drupal\Core\Entity\Display\EntityViewDisplayInterface; use Drupal\Core\Entity\RevisionableInterface; use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\quickedit\Entity\QuickEditLayoutBuilderEntityViewDisplay; +use Drupal\quickedit\LayoutBuilderIntegration; /** * Implements hook_help(). @@ -174,10 +176,28 @@ function quickedit_preprocess_field(&$variables) { } } +/** + * Implements hook_entity_type_alter(). + */ +function quickedit_entity_type_alter(array &$entity_types) { + if (\Drupal::moduleHandler()->moduleExists('layout_builder')) { + /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */ + if ($entity_types['entity_view_display']->getClass() === 'Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay\LayoutBuilderEntityViewDisplay') { + $entity_types['entity_view_display']->setClass(QuickEditLayoutBuilderEntityViewDisplay::class); + } + } +} + /** * Implements hook_entity_view_alter(). */ function quickedit_entity_view_alter(&$build, EntityInterface $entity, EntityViewDisplayInterface $display) { + if (\Drupal::moduleHandler()->moduleExists('layout_builder')) { + /** @var \Drupal\quickedit\LayoutBuilderIntegration $layout_builder_integration */ + $layout_builder_integration = \Drupal::classResolver(LayoutBuilderIntegration::class); + $layout_builder_integration->entityViewAlter($build, $entity, $display); + } + if (isset($build['#embed'])) { return; } @@ -189,3 +209,12 @@ function quickedit_entity_view_alter(&$build, EntityInterface $entity, EntityVie $build['#attributes']['data-quickedit-entity-id'] = $entity->getEntityTypeId() . '/' . $entity->id(); } + +/** + * Implements hook_quickedit_render_field(). + */ +function layout_builder_quickedit_render_field(EntityInterface $entity, $field_name, $view_mode_id, $langcode) { + /** @var \Drupal\quickedit\LayoutBuilderIntegration $layout_builder_integration */ + $layout_builder_integration = \Drupal::classResolver(LayoutBuilderIntegration::class); + return $layout_builder_integration->quickEditRenderField($entity, $field_name, $view_mode_id, $langcode); +} diff --git a/core/modules/quickedit/src/Entity/QuickEditLayoutBuilderEntityViewDisplay.php b/core/modules/quickedit/src/Entity/QuickEditLayoutBuilderEntityViewDisplay.php new file mode 100644 index 0000000000000000000000000000000000000000..3dd4eb9b106271d1c3a80b81c20d59bc262b262b --- /dev/null +++ b/core/modules/quickedit/src/Entity/QuickEditLayoutBuilderEntityViewDisplay.php @@ -0,0 +1,68 @@ +<?php + +namespace Drupal\quickedit\Entity; + +use Drupal\Component\Plugin\ConfigurableInterface; +use Drupal\Component\Plugin\DerivativeInspectionInterface; +use Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay; +use Drupal\quickedit\LayoutBuilderIntegration; + +/** + * Provides an entity view display entity that has a layout with quickedit. + */ +class QuickEditLayoutBuilderEntityViewDisplay extends LayoutBuilderEntityViewDisplay { + + /** + * {@inheritdoc} + */ + public function getComponent($name) { + if ($this->isLayoutBuilderEnabled() && $section_component = $this->getQuickEditSectionComponent()) { + $plugin = $section_component->getPlugin(); + if ($plugin instanceof ConfigurableInterface) { + $configuration = $plugin->getConfiguration(); + if (isset($configuration['formatter'])) { + return $configuration['formatter']; + } + } + } + return parent::getComponent($name); + } + + /** + * Returns the Quick Edit formatter settings. + * + * @return \Drupal\layout_builder\SectionComponent|null + * The section component if it is available. + * + * @see \Drupal\quickedit\LayoutBuilderIntegration::entityViewAlter() + * @see \Drupal\quickedit\MetadataGenerator::generateFieldMetadata() + */ + private function getQuickEditSectionComponent() { + // To determine the Quick Edit view_mode ID we need an originalMode set. + if ($original_mode = $this->getOriginalMode()) { + $parts = explode('-', $original_mode); + // The Quick Edit view mode ID is created by + // \Drupal\quickedit\LayoutBuilderIntegration::entityViewAlter() + // concatenating together the information we need to retrieve the Layout + // Builder component. It follows the structure prescribed by the + // documentation of hook_quickedit_render_field(). + if (count($parts) === 6 && $parts[0] === 'layout_builder') { + [, $delta, $component_uuid, $entity_id] = LayoutBuilderIntegration::deconstructViewModeId($original_mode); + $entity = $this->entityTypeManager()->getStorage($this->getTargetEntityTypeId())->load($entity_id); + $sections = $this->getEntitySections($entity); + if (isset($sections[$delta])) { + $component = $sections[$delta]->getComponent($component_uuid); + $plugin = $component->getPlugin(); + // We only care about FieldBlock because these are only components + // that provide Quick Edit integration: Quick Edit enables in-place + // editing of fields of entities, not of anything else. + if ($plugin instanceof DerivativeInspectionInterface && $plugin->getBaseId() === 'field_block') { + return $component; + } + } + } + } + return NULL; + } + +} diff --git a/core/modules/quickedit/src/LayoutBuilderIntegration.php b/core/modules/quickedit/src/LayoutBuilderIntegration.php new file mode 100644 index 0000000000000000000000000000000000000000..49e229b98540c4cd7f4a8bf5ee03cbbeb24e3d83 --- /dev/null +++ b/core/modules/quickedit/src/LayoutBuilderIntegration.php @@ -0,0 +1,321 @@ +<?php + +namespace Drupal\quickedit; + +use Drupal\Component\Utility\NestedArray; +use Drupal\Core\Cache\CacheableMetadata; +use Drupal\Core\DependencyInjection\ContainerInjectionInterface; +use Drupal\Core\Entity\Display\EntityViewDisplayInterface; +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Entity\FieldableEntityInterface; +use Drupal\Core\Logger\LoggerChannelTrait; +use Drupal\Core\Plugin\Context\Context; +use Drupal\Core\Plugin\Context\ContextDefinition; +use Drupal\Core\Plugin\Context\EntityContext; +use Drupal\Core\Render\Element; +use Drupal\Core\Session\AccountInterface; +use Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Helper methods for Layout Builder module integration. + * + * @internal + * This is an internal utility class wrapping hook implementations. + */ +class LayoutBuilderIntegration implements ContainerInjectionInterface { + + use LoggerChannelTrait; + + /** + * The section storage manager. + * + * @var \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface + */ + protected $sectionStorageManager; + + /** + * The current user. + * + * @var \Drupal\Core\Session\AccountInterface + */ + protected $currentUser; + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * Constructs a new LayoutBuilderIntegration object. + * + * @param \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface $section_storage_manager + * The section storage manager. + * @param \Drupal\Core\Session\AccountInterface $current_user + * The current user. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + */ + public function __construct(SectionStorageManagerInterface $section_storage_manager, AccountInterface $current_user, EntityTypeManagerInterface $entity_type_manager) { + $this->sectionStorageManager = $section_storage_manager; + $this->currentUser = $current_user; + $this->entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('plugin.manager.layout_builder.section_storage'), + $container->get('current_user'), + $container->get('entity_type.manager') + ); + } + + /** + * Alters the entity view build for Layout Builder compatibility. + * + * When rendering fields outside of normal view modes, Quick Edit requires + * that modules identify themselves with a view mode ID in the format + * [module_name]-[information the module needs to rerender], as prescribed by + * hook_quickedit_render_field(). + * + * @param array $build + * The built entity render array. + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity. + * @param \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display + * The entity view display. + * + * @see hook_quickedit_render_field() + * @see layout_builder_quickedit_render_field() + */ + public function entityViewAlter(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display) { + if (!$entity instanceof FieldableEntityInterface || !isset($build['_layout_builder'])) { + return; + } + + $build['#cache']['contexts'][] = 'user.permissions'; + if (!$this->currentUser->hasPermission('access in-place editing')) { + return; + } + + $cacheable_metadata = CacheableMetadata::createFromRenderArray($build); + $section_list = $this->sectionStorageManager->findByContext( + [ + 'display' => EntityContext::fromEntity($display), + 'entity' => EntityContext::fromEntity($entity), + 'view_mode' => new Context(new ContextDefinition('string'), $display->getMode()), + ], + $cacheable_metadata + ); + $cacheable_metadata->applyTo($build); + + if (empty($section_list)) { + return; + } + + // Create a hash of the sections and use it in the unique Quick Edit view + // mode ID. Any changes to the sections will result in a different hash, + // forcing Quick Edit's JavaScript to recognize any changes and retrieve + // up-to-date metadata. + $sections_hash = hash('sha256', serialize($section_list->getSections())); + + // Track each component by their plugin ID, delta, region, and UUID. + $plugin_ids_to_update = []; + foreach (Element::children($build['_layout_builder']) as $delta) { + $section = $build['_layout_builder'][$delta]; + + if (!Element::isEmpty($section)) { + /** @var \Drupal\Core\Layout\LayoutDefinition $layout */ + $layout = $section['#layout']; + $regions = $layout->getRegionNames(); + + foreach ($regions as $region) { + if (isset($section[$region])) { + foreach ($section[$region] as $uuid => $component) { + if (isset($component['#plugin_id']) && $this->supportQuickEditOnComponent($component, $entity)) { + $plugin_ids_to_update[$component['#plugin_id']][$delta][$region][$uuid] = $uuid; + } + } + } + } + } + } + + // @todo Remove when https://www.drupal.org/node/3041850 is resolved. + $plugin_ids_to_update = array_filter($plugin_ids_to_update, function ($info) { + // Delta, region, and UUID each count as one. + return count($info, COUNT_RECURSIVE) === 3; + }); + + $plugin_ids_to_update = NestedArray::mergeDeepArray($plugin_ids_to_update, TRUE); + foreach ($plugin_ids_to_update as $delta => $regions) { + foreach ($regions as $region => $uuids) { + foreach ($uuids as $uuid => $component) { + $build['_layout_builder'][$delta][$region][$uuid]['content']['#view_mode'] = static::getViewModeId($entity, $display, $delta, $uuid, $sections_hash); + } + } + } + // Alter the Quick Edit view mode ID of all fields outside of the Layout + // Builder sections to force Quick Edit to request to the field metadata. + // @todo Remove this logic in https://www.drupal.org/project/node/2966136. + foreach (Element::children($build) as $field_name) { + if ($field_name !== '_layout_builder') { + $field_build = &$build[$field_name]; + if (isset($field_build['#view_mode'])) { + $field_build['#view_mode'] = "layout_builder-{$display->getMode()}-non_component-$sections_hash"; + } + } + } + } + + /** + * Generates a Quick Edit view mode ID. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity. + * @param \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display + * The entity view display. + * @param int $delta + * The delta. + * @param string $component_uuid + * The component UUID. + * @param string $sections_hash + * The hash of the sections; must change whenever the sections change. + * + * @return string + * The Quick Edit view mode ID. + * + * @see \Drupal\quickedit\LayoutBuilderIntegration::deconstructViewModeId() + */ + private static function getViewModeId(EntityInterface $entity, EntityViewDisplayInterface $display, $delta, $component_uuid, $sections_hash) { + return implode('-', [ + 'layout_builder', + $display->getMode(), + $delta, + // Replace the dashes in the component UUID because we need to + // use dashes to join the parts. + str_replace('-', '_', $component_uuid), + $entity->id(), + $sections_hash, + ]); + } + + /** + * Deconstructs the Quick Edit view mode ID into its constituent parts. + * + * @param string $quick_edit_view_mode_id + * The Quick Edit view mode ID. + * + * @return array + * An array containing the entity view mode ID, the delta, the component + * UUID, and the entity ID. + * + * @see \Drupal\quickedit\LayoutBuilderIntegration::getViewModeId() + */ + public static function deconstructViewModeId($quick_edit_view_mode_id) { + [, $entity_view_mode_id, $delta, $component_uuid, $entity_id] = explode('-', $quick_edit_view_mode_id, 7); + return [ + $entity_view_mode_id, + // @todo Explicitly cast delta to an integer, remove this in + // https://www.drupal.org/project/drupal/issues/2984509. + (int) $delta, + // Replace the underscores with dash to get back the component UUID. + str_replace('_', '-', $component_uuid), + $entity_id, + ]; + } + + /** + * Re-renders a field rendered by Layout Builder, edited with Quick Edit. + * + * @param \Drupal\Core\Entity\FieldableEntityInterface $entity + * The entity. + * @param string $field_name + * The field name. + * @param string $quick_edit_view_mode_id + * The Quick Edit view mode ID. + * @param string $langcode + * The language code. + * + * @return array + * The re-rendered field. + */ + public function quickEditRenderField(FieldableEntityInterface $entity, $field_name, $quick_edit_view_mode_id, $langcode) { + [$entity_view_mode, $delta, $component_uuid] = static::deconstructViewModeId($quick_edit_view_mode_id); + + $entity_build = $this->entityTypeManager->getViewBuilder($entity->getEntityTypeId())->view($entity, $entity_view_mode, $langcode); + $this->buildEntityView($entity_build); + + if (isset($entity_build['_layout_builder'][$delta])) { + foreach (Element::children($entity_build['_layout_builder'][$delta]) as $region) { + if (isset($entity_build['_layout_builder'][$delta][$region][$component_uuid])) { + return $entity_build['_layout_builder'][$delta][$region][$component_uuid]['content']; + } + } + } + + $this->getLogger('layout_builder')->warning('The field "%field" failed to render.', ['%field' => $field_name]); + return []; + } + + /** + * {@inheritdoc} + * + * @todo Replace this hardcoded processing when + * https://www.drupal.org/project/drupal/issues/3041635 is resolved. + * + * @see \Drupal\Tests\EntityViewTrait::buildEntityView() + */ + private function buildEntityView(array &$elements) { + // If the default values for this element have not been loaded yet, + // populate them. + if (isset($elements['#type']) && empty($elements['#defaults_loaded'])) { + $elements += \Drupal::service('element_info')->getInfo($elements['#type']); + } + + // Make any final changes to the element before it is rendered. This means + // that the $element or the children can be altered or corrected before + // the element is rendered into the final text. + if (isset($elements['#pre_render'])) { + foreach ($elements['#pre_render'] as $callable) { + $elements = call_user_func($callable, $elements); + } + } + + // And recurse. + $children = Element::children($elements, TRUE); + foreach ($children as $key) { + $this->buildEntityView($elements[$key]); + } + } + + /** + * Determines whether a component has Quick Edit support. + * + * Only field_block components for display configurable fields should be + * supported. + * + * @param array $component + * The component render array. + * @param \Drupal\Core\Entity\FieldableEntityInterface $entity + * The entity being displayed. + * + * @return bool + * Whether Quick Edit is supported on the component. + * + * @see \Drupal\layout_builder\Plugin\Block\FieldBlock + */ + private function supportQuickEditOnComponent(array $component, FieldableEntityInterface $entity) { + if (isset($component['content']['#field_name'], $component['#base_plugin_id']) && $component['#base_plugin_id'] === 'field_block' && $entity->hasField($component['content']['#field_name'])) { + return $entity->getFieldDefinition($component['content']['#field_name'])->isDisplayConfigurable('view'); + } + return FALSE; + } + +} diff --git a/core/modules/quickedit/tests/src/Functional/Jsonapi/QuickEditLayoutBuilderEntityViewDisplayTest.php b/core/modules/quickedit/tests/src/Functional/Jsonapi/QuickEditLayoutBuilderEntityViewDisplayTest.php new file mode 100644 index 0000000000000000000000000000000000000000..0e1509b2a73b10afbc34c188ad65219de619035e --- /dev/null +++ b/core/modules/quickedit/tests/src/Functional/Jsonapi/QuickEditLayoutBuilderEntityViewDisplayTest.php @@ -0,0 +1,21 @@ +<?php + +namespace Drupal\Tests\quickedit\Functional\Jsonapi; + +use Drupal\Tests\layout_builder\Functional\Jsonapi\LayoutBuilderEntityViewDisplayTest; + +/** + * JSON:API integration test for the "EntityViewDisplay" config entity type. + * + * @group jsonapi + * @group layout_builder + * @group quickedit + */ +class QuickEditLayoutBuilderEntityViewDisplayTest extends LayoutBuilderEntityViewDisplayTest { + + /** + * {@inheritdoc} + */ + protected static $modules = ['quickedit']; + +} diff --git a/core/modules/quickedit/tests/src/Functional/Rest/QuickEditLayoutBuilderEntityViewDisplayJsonAnonTest.php b/core/modules/quickedit/tests/src/Functional/Rest/QuickEditLayoutBuilderEntityViewDisplayJsonAnonTest.php new file mode 100644 index 0000000000000000000000000000000000000000..c138148ae92558b19827dcd9b0160c4a6cf03298 --- /dev/null +++ b/core/modules/quickedit/tests/src/Functional/Rest/QuickEditLayoutBuilderEntityViewDisplayJsonAnonTest.php @@ -0,0 +1,19 @@ +<?php + +namespace Drupal\Tests\quickedit\Functional\Rest; + +use Drupal\Tests\layout_builder\Functional\Rest\LayoutBuilderEntityViewDisplayJsonAnonTest; + +/** + * @group quickedit + * @group layout_builder + * @group rest + */ +class QuickEditLayoutBuilderEntityViewDisplayJsonAnonTest extends LayoutBuilderEntityViewDisplayJsonAnonTest { + + /** + * {@inheritdoc} + */ + protected static $modules = ['quickedit']; + +} diff --git a/core/modules/quickedit/tests/src/Functional/Rest/QuickEditLayoutBuilderEntityViewDisplayJsonBasicAuthTest.php b/core/modules/quickedit/tests/src/Functional/Rest/QuickEditLayoutBuilderEntityViewDisplayJsonBasicAuthTest.php new file mode 100644 index 0000000000000000000000000000000000000000..a3acf46eead6f7ea403aa04351585031697f7b7a --- /dev/null +++ b/core/modules/quickedit/tests/src/Functional/Rest/QuickEditLayoutBuilderEntityViewDisplayJsonBasicAuthTest.php @@ -0,0 +1,19 @@ +<?php + +namespace Drupal\Tests\quickedit\Functional\Rest; + +use Drupal\Tests\layout_builder\Functional\Rest\LayoutBuilderEntityViewDisplayJsonBasicAuthTest; + +/** + * @group quickedit + * @group layout_builder + * @group rest + */ +class QuickEditLayoutBuilderEntityViewDisplayJsonBasicAuthTest extends LayoutBuilderEntityViewDisplayJsonBasicAuthTest { + + /** + * {@inheritdoc} + */ + protected static $modules = ['quickedit']; + +} diff --git a/core/modules/quickedit/tests/src/Functional/Rest/QuickEditLayoutBuilderEntityViewDisplayJsonCookieTest.php b/core/modules/quickedit/tests/src/Functional/Rest/QuickEditLayoutBuilderEntityViewDisplayJsonCookieTest.php new file mode 100644 index 0000000000000000000000000000000000000000..ffd07b021d4061e166cf9c103145fefd322549d4 --- /dev/null +++ b/core/modules/quickedit/tests/src/Functional/Rest/QuickEditLayoutBuilderEntityViewDisplayJsonCookieTest.php @@ -0,0 +1,19 @@ +<?php + +namespace Drupal\Tests\quickedit\Functional\Rest; + +use Drupal\Tests\layout_builder\Functional\Rest\LayoutBuilderEntityViewDisplayJsonCookieTest; + +/** + * @group quickedit + * @group layout_builder + * @group rest + */ +class QuickEditLayoutBuilderEntityViewDisplayJsonCookieTest extends LayoutBuilderEntityViewDisplayJsonCookieTest { + + /** + * {@inheritdoc} + */ + protected static $modules = ['quickedit']; + +} diff --git a/core/modules/quickedit/tests/src/Functional/Rest/QuickEditLayoutBuilderEntityViewDisplayXmlAnonTest.php b/core/modules/quickedit/tests/src/Functional/Rest/QuickEditLayoutBuilderEntityViewDisplayXmlAnonTest.php new file mode 100644 index 0000000000000000000000000000000000000000..87c6a72639f0e5b22a657d7286765056dd37bba2 --- /dev/null +++ b/core/modules/quickedit/tests/src/Functional/Rest/QuickEditLayoutBuilderEntityViewDisplayXmlAnonTest.php @@ -0,0 +1,19 @@ +<?php + +namespace Drupal\Tests\quickedit\Functional\Rest; + +use Drupal\Tests\layout_builder\Functional\Rest\LayoutBuilderEntityViewDisplayXmlAnonTest; + +/** + * @group quickedit + * @group layout_builder + * @group rest + */ +class QuickEditLayoutBuilderEntityViewDisplayXmlAnonTest extends LayoutBuilderEntityViewDisplayXmlAnonTest { + + /** + * {@inheritdoc} + */ + protected static $modules = ['quickedit']; + +} diff --git a/core/modules/quickedit/tests/src/Functional/Rest/QuickEditLayoutBuilderEntityViewDisplayXmlBasicAuthTest.php b/core/modules/quickedit/tests/src/Functional/Rest/QuickEditLayoutBuilderEntityViewDisplayXmlBasicAuthTest.php new file mode 100644 index 0000000000000000000000000000000000000000..8640666302a51e72e9807ac439d4d2217f43d2eb --- /dev/null +++ b/core/modules/quickedit/tests/src/Functional/Rest/QuickEditLayoutBuilderEntityViewDisplayXmlBasicAuthTest.php @@ -0,0 +1,19 @@ +<?php + +namespace Drupal\Tests\quickedit\Functional\Rest; + +use Drupal\Tests\layout_builder\Functional\Rest\LayoutBuilderEntityViewDisplayXmlBasicAuthTest; + +/** + * @group quickedit + * @group layout_builder + * @group rest + */ +class QuickEditLayoutBuilderEntityViewDisplayXmlBasicAuthTest extends LayoutBuilderEntityViewDisplayXmlBasicAuthTest { + + /** + * {@inheritdoc} + */ + protected static $modules = ['quickedit']; + +} diff --git a/core/modules/quickedit/tests/src/Functional/Rest/QuickEditLayoutBuilderEntityViewDisplayXmlCookieTest.php b/core/modules/quickedit/tests/src/Functional/Rest/QuickEditLayoutBuilderEntityViewDisplayXmlCookieTest.php new file mode 100644 index 0000000000000000000000000000000000000000..699efe177ecf8f8badc5d0c0c7e7e0e375b7396b --- /dev/null +++ b/core/modules/quickedit/tests/src/Functional/Rest/QuickEditLayoutBuilderEntityViewDisplayXmlCookieTest.php @@ -0,0 +1,19 @@ +<?php + +namespace Drupal\Tests\quickedit\Functional\Rest; + +use Drupal\Tests\layout_builder\Functional\Rest\LayoutBuilderEntityViewDisplayXmlCookieTest; + +/** + * @group quickedit + * @group layout_builder + * @group rest + */ +class QuickEditLayoutBuilderEntityViewDisplayXmlCookieTest extends LayoutBuilderEntityViewDisplayXmlCookieTest { + + /** + * {@inheritdoc} + */ + protected static $modules = ['quickedit']; + +} diff --git a/core/modules/quickedit/tests/src/FunctionalJavascript/QuickEditIntegrationTest.php b/core/modules/quickedit/tests/src/FunctionalJavascript/LayoutBuilderIntegrationTest.php similarity index 99% rename from core/modules/quickedit/tests/src/FunctionalJavascript/QuickEditIntegrationTest.php rename to core/modules/quickedit/tests/src/FunctionalJavascript/LayoutBuilderIntegrationTest.php index a15bb9ac5b978851c5a1911778e9abf19dbc741c..7bc4e2a4ef70d8872d4b6ac66ddc104ece5cec96 100644 --- a/core/modules/quickedit/tests/src/FunctionalJavascript/QuickEditIntegrationTest.php +++ b/core/modules/quickedit/tests/src/FunctionalJavascript/LayoutBuilderIntegrationTest.php @@ -14,7 +14,7 @@ /** * @group quickedit */ -class QuickEditIntegrationTest extends QuickEditJavascriptTestBase { +class LayoutBuilderIntegrationTest extends QuickEditJavascriptTestBase { use EntityReferenceTestTrait;