diff --git a/core/modules/layout_builder/layout_builder.module b/core/modules/layout_builder/layout_builder.module index 8d19c35badc6b0d168c5651e273ab7c9038cede5..d746d343254b3611c843df51de5f37f8761a1d6f 100644 --- a/core/modules/layout_builder/layout_builder.module +++ b/core/modules/layout_builder/layout_builder.module @@ -147,6 +147,27 @@ function layout_builder_entity_view_alter(array &$build, EntityInterface $entity } } } + + $route_name = \Drupal::routeMatch()->getRouteName(); + + // If the entity is displayed within a Layout Builder block and the current + // route is in the Layout Builder UI, then remove all contextual link + // placeholders. + if ($display instanceof LayoutBuilderEntityViewDisplay && strpos($route_name, 'layout_builder.') === 0) { + unset($build['#contextual_links']); + } +} + +/** + * Implements hook_entity_build_defaults_alter(). + */ +function layout_builder_entity_build_defaults_alter(array &$build, EntityInterface $entity, $view_mode) { + // Contextual links are removed for entities viewed in Layout Builder's UI. + // The route.name.is_layout_builder_ui cache context accounts for this + // difference. + // @see layout_builder_entity_view_alter() + // @see \Drupal\layout_builder\Cache\LayoutBuilderUiCacheContext + $build['#cache']['contexts'][] = 'route.name.is_layout_builder_ui'; } /** diff --git a/core/modules/layout_builder/layout_builder.services.yml b/core/modules/layout_builder/layout_builder.services.yml index 65fb66a895c78d6f7f1f530a10a4846bc643b738..6b40cf9e3beb321e1dfaa72b25317f0253cadb28 100644 --- a/core/modules/layout_builder/layout_builder.services.yml +++ b/core/modules/layout_builder/layout_builder.services.yml @@ -29,6 +29,11 @@ services: arguments: ['@current_route_match'] tags: - { name: cache.context} + cache_context.route.name.is_layout_builder_ui: + class: Drupal\layout_builder\Cache\LayoutBuilderUiCacheContext + arguments: ['@current_route_match'] + tags: + - { name: cache.context } layout_builder.sample_entity_generator: class: Drupal\layout_builder\Entity\LayoutBuilderSampleEntityGenerator arguments: ['@tempstore.shared', '@entity_type.manager'] diff --git a/core/modules/layout_builder/src/Cache/LayoutBuilderUiCacheContext.php b/core/modules/layout_builder/src/Cache/LayoutBuilderUiCacheContext.php new file mode 100644 index 0000000000000000000000000000000000000000..53b589452dcb8f924189c988a9913d4b3828a76c --- /dev/null +++ b/core/modules/layout_builder/src/Cache/LayoutBuilderUiCacheContext.php @@ -0,0 +1,28 @@ +<?php + +namespace Drupal\layout_builder\Cache; + +use Drupal\Core\Cache\Context\RouteNameCacheContext; + +/** + * Determines if an entity is being viewed in the Layout Builder UI. + * + * Cache context ID: 'route.name.is_layout_builder_ui'. + */ +class LayoutBuilderUiCacheContext extends RouteNameCacheContext { + + /** + * {@inheritdoc} + */ + public static function getLabel() { + return t('Layout Builder user interface'); + } + + /** + * {@inheritdoc} + */ + public function getContext() { + return 'is_layout_builder_ui.' . (int) (strpos($this->routeMatch->getRouteName(), 'layout_builder.') !== 0); + } + +} diff --git a/core/modules/layout_builder/src/EventSubscriber/BlockComponentRenderArray.php b/core/modules/layout_builder/src/EventSubscriber/BlockComponentRenderArray.php index 795a300b088b1c5a0ceebef24d09a1bd081cdbee..da6bf6f3dc3bee76355507247c177eff56c2fb81 100644 --- a/core/modules/layout_builder/src/EventSubscriber/BlockComponentRenderArray.php +++ b/core/modules/layout_builder/src/EventSubscriber/BlockComponentRenderArray.php @@ -11,6 +11,7 @@ use Drupal\layout_builder\Access\LayoutPreviewAccessAllowed; use Drupal\layout_builder\Event\SectionComponentBuildRenderArrayEvent; use Drupal\layout_builder\LayoutBuilderEvents; +use Drupal\views\Plugin\Block\ViewsBlock; use Symfony\Component\EventDispatcher\EventSubscriberInterface; /** @@ -90,6 +91,15 @@ public function onBuildRender(SectionComponentBuildRenderArrayEvent $event) { if ($access->isAllowed()) { $event->addCacheableDependency($block); + // @todo Revisit after https://www.drupal.org/node/3027653, as this will + // provide a better way to remove contextual links from Views blocks. + // Currently, doing this requires setting + // \Drupal\views\ViewExecutable::$showAdminLinks() to false before the + // Views block is built. + if ($block instanceof ViewsBlock && $event->inPreview()) { + $block->getViewExecutable()->setShowAdminLinks(FALSE); + } + $content = $block->build(); $is_content_empty = Element::isEmpty($content); $is_placeholder_ready = $event->inPreview() && $block instanceof PreviewFallbackInterface; diff --git a/core/modules/layout_builder/tests/modules/layout_builder_views_test/config/install/views.view.test_block_view.yml b/core/modules/layout_builder/tests/modules/layout_builder_views_test/config/install/views.view.test_block_view.yml index c666e89d68ffd35f7728739c709784832c0df8a9..3b1e9315ff8279b141e812edefa76643deea9a77 100644 --- a/core/modules/layout_builder/tests/modules/layout_builder_views_test/config/install/views.view.test_block_view.yml +++ b/core/modules/layout_builder/tests/modules/layout_builder_views_test/config/install/views.view.test_block_view.yml @@ -1,6 +1,8 @@ langcode: en status: true dependencies: + config: + - core.entity_view_mode.node.teaser module: - node - user @@ -175,3 +177,30 @@ display: - 'user.node_grants:view' - user.permissions tags: { } + block_2: + display_plugin: block + id: block_2 + display_title: 'Teaser block' + position: 2 + display_options: + display_extenders: { } + display_description: '' + style: + type: default + options: { } + defaults: + style: false + row: false + row: + type: 'entity:node' + options: + relationship: none + view_mode: teaser + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - 'user.node_grants:view' + - user.permissions + tags: { } diff --git a/core/modules/layout_builder/tests/src/FunctionalJavascript/ContextualLinksTest.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/ContextualLinksTest.php new file mode 100644 index 0000000000000000000000000000000000000000..db398a096f41c7d54ce337eb99d7c060293ecf68 --- /dev/null +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/ContextualLinksTest.php @@ -0,0 +1,167 @@ +<?php + +namespace Drupal\Tests\layout_builder\FunctionalJavascript; + +use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait; + +/** + * Test contextual links compatibility with the Layout Builder. + * + * @group layout_builder + */ +class ContextualLinksTest extends WebDriverTestBase { + + use AssertPageCacheContextsAndTagsTrait; + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'views', + 'views_ui', + 'layout_builder', + 'layout_builder_views_test', + 'layout_test', + 'layout_builder_test_css_transitions', + 'block', + 'node', + 'contextual', + ]; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $user = $this->drupalCreateUser([ + 'configure any layout', + 'administer node display', + 'administer node fields', + 'access contextual links', + 'administer nodes', + 'bypass node access', + 'administer views', + ]); + $user->save(); + $this->drupalLogin($user); + $this->createContentType(['type' => 'bundle_with_section_field']); + + $this->createNode([ + 'type' => 'bundle_with_section_field', + 'body' => [ + [ + 'value' => 'The node body', + ], + ], + ]); + } + + /** + * Tests that the contextual links inside Layout Builder are removed. + */ + public function testContextualLinks() { + $page = $this->getSession()->getPage(); + + $field_ui_prefix = 'admin/structure/types/manage/bundle_with_section_field'; + + // Enable Layout Builder and overrides. + $this->drupalPostForm( + "$field_ui_prefix/display/default", + ['layout[enabled]' => TRUE, 'layout[allow_custom]' => TRUE], + 'Save' + ); + + $this->drupalGet('node/1/layout'); + + // Add a block that includes an entity contextual link. + $this->addBlock('Test Block View: Teaser block'); + + // Add a block that includes a views contextual link. + $this->addBlock('Recent content'); + + // Ensure the contextual links are correct before the layout is saved. + $this->assertCorrectContextualLinksInUi(); + + // Ensure the contextual links are correct when the Layout Builder is loaded + // after being saved. + $page->hasButton('Save layout'); + $page->pressButton('Save layout'); + $this->drupalGet('node/1/layout'); + $this->assertCorrectContextualLinksInUi(); + + $this->drupalGet('node/1'); + $this->assertCorrectContextualLinksInNode(); + } + + /** + * Adds block to the layout via Layout Builder's UI. + * + * @param string $block_name + * The block name as it appears in the Add Block form. + */ + protected function addBlock($block_name) { + $assert_session = $this->assertSession(); + $page = $this->getSession()->getPage(); + + $assert_session->linkExists('Add Block'); + $page->clickLink('Add Block'); + $assert_session->assertWaitOnAjaxRequest(); + $this->assertNotEmpty($assert_session->waitForElementVisible('css', "#drupal-off-canvas a:contains('$block_name')")); + $page->clickLink($block_name); + $this->assertNotEmpty($assert_session->waitForElementVisible('css', '[data-drupal-selector=\'edit-actions-submit\']')); + $page->pressButton('Add Block'); + $this->waitForNoElement('#drupal-off-canvas'); + $assert_session->assertWaitOnAjaxRequest(); + } + + /** + * Asserts the contextual links are correct in Layout Builder UI. + */ + protected function assertCorrectContextualLinksInUi() { + $assert_session = $this->assertSession(); + $page = $this->getSession()->getPage(); + $this->assertNotEmpty($assert_session->waitForElementVisible('css', '.block-views-blocktest-block-view-block-2')); + + $layout_builder_specific_contextual_links = $page->findAll('css', '[data-contextual-id*=\'layout_builder_block:\']'); + $this->assertNotEmpty($layout_builder_specific_contextual_links); + + // Confirms Layout Builder contextual links are the only contextual links + // inside the Layout Builder UI. + $this->assertSameSize($layout_builder_specific_contextual_links, $page->findAll('css', '#layout-builder [data-contextual-id]')); + } + + /** + * Asserts the contextual links are correct on the canonical entity route. + */ + protected function assertCorrectContextualLinksInNode() { + $assert_session = $this->assertSession(); + $page = $this->getSession()->getPage(); + + $this->assertNotEmpty($assert_session->waitForElementVisible('css', '[data-contextual-id]')); + + // Ensure that no Layout Builder contextual links are visible on node view. + $this->assertEmpty($page->findAll('css', '[data-contextual-id*=\'layout_builder_block:\']')); + + // Ensure that the contextual links that are hidden in Layout Builder UI + // are visible on node view. + $this->assertNotEmpty($page->findAll('css', '.layout-content [data-contextual-id]')); + } + + /** + * Waits for an element to be removed from the page. + * + * @param string $selector + * CSS selector. + * @param int $timeout + * (optional) Timeout in milliseconds, defaults to 10000. + * + * @todo Remove in https://www.drupal.org/node/2892440. + */ + protected function waitForNoElement($selector, $timeout = 10000) { + $condition = "(typeof jQuery !== 'undefined' && jQuery('$selector').length === 0)"; + $this->assertJsCondition($condition, $timeout); + } + +} diff --git a/core/modules/views/src/Plugin/Block/ViewsBlockBase.php b/core/modules/views/src/Plugin/Block/ViewsBlockBase.php index 9ed369c045dc8a680f730bb7758f49624f0d6e10..92691c3b15b288d949087ec65b96e3476770f5f9 100644 --- a/core/modules/views/src/Plugin/Block/ViewsBlockBase.php +++ b/core/modules/views/src/Plugin/Block/ViewsBlockBase.php @@ -213,4 +213,18 @@ protected function addContextualLinks(&$output, $block_type = 'block') { } } + /** + * Gets the view executable. + * + * @return \Drupal\views\ViewExecutable + * The view executable. + * + * @todo revisit after https://www.drupal.org/node/3027653. This method was + * added in https://www.drupal.org/node/3002608, but should not be + * necessary once block plugins can determine if they are being previewed. + */ + public function getViewExecutable() { + return $this->view; + } + }