diff --git a/core/modules/workspaces/src/WorkspacesLazyBuilders.php b/core/modules/workspaces/src/WorkspacesLazyBuilders.php new file mode 100644 index 0000000000000000000000000000000000000000..03d1f398d36925b403b85f09259af2ff4800dfab --- /dev/null +++ b/core/modules/workspaces/src/WorkspacesLazyBuilders.php @@ -0,0 +1,87 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\workspaces; + +use Drupal\Component\Serialization\Json; +use Drupal\Core\Render\ElementInfoManagerInterface; +use Drupal\Core\Security\TrustedCallbackInterface; +use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\Core\Url; + +/** + * Defines a service for workspaces #lazy_builder callbacks. + * + * @internal + */ +final class WorkspacesLazyBuilders implements TrustedCallbackInterface { + + use StringTranslationTrait; + + public function __construct( + protected readonly WorkspaceManagerInterface $workspaceManager, + protected readonly ElementInfoManagerInterface $elementInfo, + ) {} + + /** + * Lazy builder callback for rendering the workspace toolbar tab. + * + * @return array + * A render array. + */ + public function renderToolbarTab(): array { + $active_workspace = $this->workspaceManager->getActiveWorkspace(); + + $build = [ + '#type' => 'link', + '#title' => $active_workspace ? $active_workspace->label() : $this->t('Live'), + '#url' => Url::fromRoute('entity.workspace.collection', [], ['query' => \Drupal::destination()->getAsArray()]), + '#attributes' => [ + 'title' => t('Switch workspace'), + 'class' => [ + 'toolbar-item', + 'toolbar-icon', + 'toolbar-icon-workspace', + 'use-ajax', + ], + 'data-dialog-type' => 'dialog', + 'data-dialog-renderer' => 'off_canvas_top', + 'data-dialog-options' => Json::encode([ + 'height' => 161, + 'classes' => [ + 'ui-dialog' => 'workspaces-dialog', + ], + ]), + ], + '#attached' => [ + 'library' => ['workspaces/drupal.workspaces.toolbar'], + ], + '#cache' => [ + 'max-age' => 0, + ], + ]; + + // The renderer has already added element defaults by the time the lazy + // builder is run. + // @see https://www.drupal.org/project/drupal/issues/2609250 + $build += $this->elementInfo->getInfo('link'); + return $build; + } + + /** + * Render callback for the workspace toolbar tab. + */ + public static function removeTabAttributes(array $element): array { + unset($element['tab']['#attributes']); + return $element; + } + + /** + * {@inheritdoc} + */ + public static function trustedCallbacks(): array { + return ['removeTabAttributes', 'renderToolbarTab']; + } + +} diff --git a/core/modules/workspaces/tests/src/Functional/WorkspaceSwitcherTest.php b/core/modules/workspaces/tests/src/Functional/WorkspaceSwitcherTest.php index 204b148e2aa3906c5c2534ed6d0226130616d921..4c27e4f64a3fcaf7437992a0ccc7d77b2115ad1d 100644 --- a/core/modules/workspaces/tests/src/Functional/WorkspaceSwitcherTest.php +++ b/core/modules/workspaces/tests/src/Functional/WorkspaceSwitcherTest.php @@ -4,6 +4,8 @@ namespace Drupal\Tests\workspaces\Functional; +use Drupal\Core\Url; +use Drupal\dynamic_page_cache\EventSubscriber\DynamicPageCacheSubscriber; use Drupal\Tests\BrowserTestBase; use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait; @@ -20,7 +22,12 @@ class WorkspaceSwitcherTest extends BrowserTestBase { /** * {@inheritdoc} */ - protected static $modules = ['block', 'workspaces']; + protected static $modules = [ + 'block', + 'dynamic_page_cache', + 'toolbar', + 'workspaces', + ]; /** * {@inheritdoc} @@ -93,4 +100,20 @@ public function testQueryParameterNegotiator() { $this->assertCacheContext('session'); } + /** + * Tests that the toolbar workspace switcher doesn't disable the page cache. + */ + public function testToolbarSwitcherDynamicPageCache() { + $this->drupalLogin($this->drupalCreateUser([ + 'access toolbar', + 'view any workspace', + ])); + // Front-page is visited right after login. + $this->assertSession()->responseHeaderEquals(DynamicPageCacheSubscriber::HEADER, 'MISS'); + // Reload the page, it should be cached now. + $this->drupalGet(Url::fromRoute('<front>')); + $this->assertSession()->elementExists('css', '.workspaces-toolbar-tab'); + $this->assertSession()->responseHeaderEquals(DynamicPageCacheSubscriber::HEADER, 'HIT'); + } + } diff --git a/core/modules/workspaces/workspaces.module b/core/modules/workspaces/workspaces.module index e9a2b3c17e60b41a7cb85caed6a49e12e42283f7..09c91af77ad1f74a32bb69b457c18be3e1a21775 100644 --- a/core/modules/workspaces/workspaces.module +++ b/core/modules/workspaces/workspaces.module @@ -5,7 +5,6 @@ * Provides full-site preview functionality for content staging. */ -use Drupal\Component\Serialization\Json; use Drupal\Core\Cache\Cache; use Drupal\Core\Entity\EntityFormInterface; use Drupal\Core\Entity\EntityInterface; @@ -266,29 +265,20 @@ function workspaces_toolbar() { $items['workspace'] += [ '#type' => 'toolbar_item', 'tab' => [ - '#type' => 'link', - '#title' => $active_workspace ? $active_workspace->label() : t('Live'), - '#url' => Url::fromRoute('entity.workspace.collection', [], ['query' => \Drupal::destination()->getAsArray()]), - '#attributes' => [ - 'title' => t('Switch workspace'), - 'class' => ['use-ajax', 'toolbar-icon', 'toolbar-icon-workspace'], - 'data-dialog-type' => 'dialog', - 'data-dialog-renderer' => 'off_canvas_top', - 'data-dialog-options' => Json::encode([ - 'height' => 161, - 'classes' => [ - 'ui-dialog' => 'workspaces-dialog', - ], - ]), + '#lazy_builder' => ['workspaces.lazy_builders:renderToolbarTab', []], + '#create_placeholder' => TRUE, + '#lazy_builder_preview' => [ + '#type' => 'link', + '#title' => $active_workspace ? $active_workspace->label() : t('Live'), + '#url' => Url::fromRoute('entity.workspace.collection'), + '#attributes' => [ + 'class' => ['toolbar-tray-lazy-placeholder-link'], + ], ], - '#cache' => ['tags' => $active_workspace ? $active_workspace->getCacheTags() : []], ], '#wrapper_attributes' => [ 'class' => ['workspaces-toolbar-tab'], ], - '#attached' => [ - 'library' => ['workspaces/drupal.workspaces.toolbar'], - ], '#weight' => 500, ]; @@ -298,5 +288,13 @@ function workspaces_toolbar() { $items['workspace']['#wrapper_attributes']['class'][] = 'workspaces-toolbar-tab--is-default'; } + // \Drupal\toolbar\Element\ToolbarItem::preRenderToolbarItem adds an + // #attributes property to each toolbar item's tab child automatically. + // Lazy builders don't support an #attributes property so we need to + // add another render callback to remove the #attributes property. We start by + // adding the defaults, and then we append our own pre render callback. + $items['workspace'] += \Drupal::service('plugin.manager.element_info')->getInfo('toolbar_item'); + $items['workspace']['#pre_render'][] = 'workspaces.lazy_builders:removeTabAttributes'; + return $items; } diff --git a/core/modules/workspaces/workspaces.services.yml b/core/modules/workspaces/workspaces.services.yml index 9b5f3b97ce8d3c140eaf7fad28e24bf91bb53fa4..cb7cce900691569cf5b8c1809c25b65cb1734a82 100644 --- a/core/modules/workspaces/workspaces.services.yml +++ b/core/modules/workspaces/workspaces.services.yml @@ -59,6 +59,11 @@ services: parent: logger.channel_base arguments: ['workspaces'] + workspaces.lazy_builders: + class: Drupal\workspaces\WorkspacesLazyBuilders + arguments: [ '@workspaces.manager', '@plugin.manager.element_info' ] + Drupal\workspaces\WorkspacesLazyBuilders: '@workspaces.lazy_builders' + workspaces.entity.query.sql: decorates: entity.query.sql class: Drupal\workspaces\EntityQuery\QueryFactory