From bf22a79551e20ae664ccbc6b94346cfdeca89a98 Mon Sep 17 00:00:00 2001 From: catch <catch@35733.no-reply.drupal.org> Date: Thu, 11 Apr 2024 09:44:10 +0100 Subject: [PATCH] Issue #3000749 by amateescu, s_leu, dragos-dumi, tim.plunkett, jeremylichtman, smithmilner, velocis: Layout builder overrides on a single content item not allowed in a workspace --- .../config/schema/layout_builder.schema.yml | 3 + .../src/Form/ConfigureBlockFormBase.php | 2 + .../src/Form/ConfigureSectionForm.php | 2 + .../src/Form/DiscardLayoutChangesForm.php | 3 + .../src/Form/LayoutBuilderDisableForm.php | 3 + .../src/Form/LayoutRebuildConfirmFormBase.php | 2 + .../layout_builder/src/Form/MoveBlockForm.php | 2 + .../src/Form/OverridesEntityForm.php | 2 + .../src/Form/RevertOverridesForm.php | 3 + .../src/Form/WorkspaceSafeFormTrait.php | 81 ++++++++ .../src/InlineBlockEntityOperations.php | 21 +-- .../src/Plugin/Block/InlineBlock.php | 2 + .../InlineBlockTestBase.php | 13 +- ...WorkspacesLayoutBuilderIntegrationTest.php | 178 ++++++++++++++++++ 14 files changed, 293 insertions(+), 24 deletions(-) create mode 100644 core/modules/layout_builder/src/Form/WorkspaceSafeFormTrait.php create mode 100644 core/modules/workspaces/tests/src/FunctionalJavascript/WorkspacesLayoutBuilderIntegrationTest.php diff --git a/core/modules/layout_builder/config/schema/layout_builder.schema.yml b/core/modules/layout_builder/config/schema/layout_builder.schema.yml index 11bfcd736126..9240ee8cb55d 100644 --- a/core/modules/layout_builder/config/schema/layout_builder.schema.yml +++ b/core/modules/layout_builder/config/schema/layout_builder.schema.yml @@ -64,6 +64,9 @@ inline_block: view_mode: type: string label: 'View mode' + block_id: + type: integer + label: 'Block ID' block_revision_id: type: integer label: 'Block revision ID' diff --git a/core/modules/layout_builder/src/Form/ConfigureBlockFormBase.php b/core/modules/layout_builder/src/Form/ConfigureBlockFormBase.php index 76f2526d0410..4be13361aff4 100644 --- a/core/modules/layout_builder/src/Form/ConfigureBlockFormBase.php +++ b/core/modules/layout_builder/src/Form/ConfigureBlockFormBase.php @@ -35,6 +35,7 @@ abstract class ConfigureBlockFormBase extends FormBase implements BaseFormIdInte use ContextAwarePluginAssignmentTrait; use LayoutBuilderContextTrait; use LayoutRebuildTrait; + use WorkspaceSafeFormTrait; /** * The plugin being configured. @@ -163,6 +164,7 @@ public function doBuildForm(array $form, FormStateInterface $form_state, Section $this->delta = $delta; $this->uuid = $component->getUuid(); $this->block = $component->getPlugin(); + $this->markWorkspaceSafe($form_state); $form_state->setTemporaryValue('gathered_contexts', $this->getPopulatedContexts($section_storage)); diff --git a/core/modules/layout_builder/src/Form/ConfigureSectionForm.php b/core/modules/layout_builder/src/Form/ConfigureSectionForm.php index 7f31ec571783..42166abc1fbf 100644 --- a/core/modules/layout_builder/src/Form/ConfigureSectionForm.php +++ b/core/modules/layout_builder/src/Form/ConfigureSectionForm.php @@ -32,6 +32,7 @@ class ConfigureSectionForm extends FormBase { use LayoutBuilderContextTrait; use LayoutBuilderHighlightTrait; use LayoutRebuildTrait; + use WorkspaceSafeFormTrait; /** * The layout tempstore repository. @@ -127,6 +128,7 @@ public function buildForm(array $form, FormStateInterface $form_state, SectionSt $this->delta = $delta; $this->isUpdate = is_null($plugin_id); $this->pluginId = $plugin_id; + $this->markWorkspaceSafe($form_state); $section = $this->getCurrentSection(); diff --git a/core/modules/layout_builder/src/Form/DiscardLayoutChangesForm.php b/core/modules/layout_builder/src/Form/DiscardLayoutChangesForm.php index 5e4f7c250310..2befa97d3b0a 100644 --- a/core/modules/layout_builder/src/Form/DiscardLayoutChangesForm.php +++ b/core/modules/layout_builder/src/Form/DiscardLayoutChangesForm.php @@ -17,6 +17,8 @@ */ class DiscardLayoutChangesForm extends ConfirmFormBase { + use WorkspaceSafeFormTrait; + /** * The layout tempstore repository. * @@ -87,6 +89,7 @@ public function getCancelUrl() { */ public function buildForm(array $form, FormStateInterface $form_state, SectionStorageInterface $section_storage = NULL) { $this->sectionStorage = $section_storage; + $this->markWorkspaceSafe($form_state); // Mark this as an administrative page for JavaScript ("Back to site" link). $form['#attached']['drupalSettings']['path']['currentPathIsAdmin'] = TRUE; return parent::buildForm($form, $form_state); diff --git a/core/modules/layout_builder/src/Form/LayoutBuilderDisableForm.php b/core/modules/layout_builder/src/Form/LayoutBuilderDisableForm.php index 1b30ca932cb7..2f7173fb30a7 100644 --- a/core/modules/layout_builder/src/Form/LayoutBuilderDisableForm.php +++ b/core/modules/layout_builder/src/Form/LayoutBuilderDisableForm.php @@ -18,6 +18,8 @@ */ class LayoutBuilderDisableForm extends ConfirmFormBase { + use WorkspaceSafeFormTrait; + /** * The layout tempstore repository. * @@ -92,6 +94,7 @@ public function buildForm(array $form, FormStateInterface $form_state, SectionSt } $this->sectionStorage = $section_storage; + $this->markWorkspaceSafe($form_state); // Mark this as an administrative page for JavaScript ("Back to site" link). $form['#attached']['drupalSettings']['path']['currentPathIsAdmin'] = TRUE; return parent::buildForm($form, $form_state); diff --git a/core/modules/layout_builder/src/Form/LayoutRebuildConfirmFormBase.php b/core/modules/layout_builder/src/Form/LayoutRebuildConfirmFormBase.php index 75eba575b752..8e7cbf4b450d 100644 --- a/core/modules/layout_builder/src/Form/LayoutRebuildConfirmFormBase.php +++ b/core/modules/layout_builder/src/Form/LayoutRebuildConfirmFormBase.php @@ -22,6 +22,7 @@ abstract class LayoutRebuildConfirmFormBase extends ConfirmFormBase { use AjaxFormHelperTrait; use LayoutBuilderHighlightTrait; use LayoutRebuildTrait; + use WorkspaceSafeFormTrait; /** * The layout tempstore repository. @@ -77,6 +78,7 @@ public function buildForm(array $form, FormStateInterface $form_state, SectionSt $this->sectionStorage = $section_storage; $this->delta = $delta; + $this->markWorkspaceSafe($form_state); $form = parent::buildForm($form, $form_state); if ($this->isAjax()) { diff --git a/core/modules/layout_builder/src/Form/MoveBlockForm.php b/core/modules/layout_builder/src/Form/MoveBlockForm.php index beb3aa975725..574f2c5d0d73 100644 --- a/core/modules/layout_builder/src/Form/MoveBlockForm.php +++ b/core/modules/layout_builder/src/Form/MoveBlockForm.php @@ -24,6 +24,7 @@ class MoveBlockForm extends FormBase { use LayoutBuilderContextTrait; use LayoutBuilderHighlightTrait; use LayoutRebuildTrait; + use WorkspaceSafeFormTrait; /** * The section storage. @@ -117,6 +118,7 @@ public function buildForm(array $form, FormStateInterface $form_state, SectionSt $this->delta = $delta; $this->uuid = $uuid; $this->region = $region; + $this->markWorkspaceSafe($form_state); $form['#attributes']['data-layout-builder-target-highlight-id'] = $this->blockUpdateHighlightId($uuid); diff --git a/core/modules/layout_builder/src/Form/OverridesEntityForm.php b/core/modules/layout_builder/src/Form/OverridesEntityForm.php index 87f647e31a4e..77743a3c585d 100644 --- a/core/modules/layout_builder/src/Form/OverridesEntityForm.php +++ b/core/modules/layout_builder/src/Form/OverridesEntityForm.php @@ -25,6 +25,7 @@ class OverridesEntityForm extends ContentEntityForm { use PreviewToggleTrait; use LayoutBuilderEntityFormTrait; + use WorkspaceSafeFormTrait; /** * Layout tempstore repository. @@ -90,6 +91,7 @@ protected function init(FormStateInterface $form_state) { */ public function buildForm(array $form, FormStateInterface $form_state, SectionStorageInterface $section_storage = NULL) { $this->sectionStorage = $section_storage; + $this->markWorkspaceSafe($form_state); $form = parent::buildForm($form, $form_state); $form['#attributes']['class'][] = 'layout-builder-form'; diff --git a/core/modules/layout_builder/src/Form/RevertOverridesForm.php b/core/modules/layout_builder/src/Form/RevertOverridesForm.php index c852504f5c9c..9375904e0e62 100644 --- a/core/modules/layout_builder/src/Form/RevertOverridesForm.php +++ b/core/modules/layout_builder/src/Form/RevertOverridesForm.php @@ -18,6 +18,8 @@ */ class RevertOverridesForm extends ConfirmFormBase { + use WorkspaceSafeFormTrait; + /** * The layout tempstore repository. * @@ -99,6 +101,7 @@ public function buildForm(array $form, FormStateInterface $form_state, SectionSt } $this->sectionStorage = $section_storage; + $this->markWorkspaceSafe($form_state); // Mark this as an administrative page for JavaScript ("Back to site" link). $form['#attached']['drupalSettings']['path']['currentPathIsAdmin'] = TRUE; return parent::buildForm($form, $form_state); diff --git a/core/modules/layout_builder/src/Form/WorkspaceSafeFormTrait.php b/core/modules/layout_builder/src/Form/WorkspaceSafeFormTrait.php new file mode 100644 index 000000000000..f93986b99c8f --- /dev/null +++ b/core/modules/layout_builder/src/Form/WorkspaceSafeFormTrait.php @@ -0,0 +1,81 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\layout_builder\Form; + +use Drupal\Core\Form\FormStateInterface; +use Drupal\layout_builder\SectionStorageInterface; +use Drupal\workspaces\WorkspaceInformationInterface; + +/** + * Provides a trait that marks Layout Builder forms as workspace-safe. + */ +trait WorkspaceSafeFormTrait { + + /** + * The workspace information service. + */ + protected ?WorkspaceInformationInterface $workspaceInfo = NULL; + + /** + * Marks a form as workspace-safe, if possible. + * + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state object. + */ + protected function markWorkspaceSafe(FormStateInterface $form_state): void { + if (!\Drupal::hasService('workspaces.information')) { + return; + } + + $section_storage = $this->sectionStorage ?: $this->getSectionStorageFromFormState($form_state); + if ($section_storage) { + $context_definitions = $section_storage->getContextDefinitions(); + if (!empty($context_definitions['entity'])) { + /** @var \Drupal\Core\Entity\EntityInterface $entity */ + $entity = $section_storage->getContext('entity')->getContextValue(); + $supported = $entity && $this->getWorkspaceInfo()->isEntitySupported($entity); + $ignored = $entity && $this->getWorkspaceInfo()->isEntityIgnored($entity); + + if ($supported || $ignored) { + $form_state->set('workspace_safe', TRUE); + } + } + } + } + + /** + * Retrieves the section storage from a form state object, if it exists. + * + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state object. + * + * @return \Drupal\layout_builder\SectionStorageInterface|null + * The section storage or NULL if it doesn't exist. + */ + protected function getSectionStorageFromFormState(FormStateInterface $form_state): ?SectionStorageInterface { + foreach ($form_state->getBuildInfo()['args'] as $argument) { + if ($argument instanceof SectionStorageInterface) { + return $argument; + } + } + + return NULL; + } + + /** + * Retrieves the workspace information service. + * + * @return \Drupal\workspaces\WorkspaceInformationInterface + * The workspace information service. + */ + protected function getWorkspaceInfo(): WorkspaceInformationInterface { + if (!$this->workspaceInfo) { + $this->workspaceInfo = \Drupal::service('workspaces.information'); + } + + return $this->workspaceInfo; + } + +} diff --git a/core/modules/layout_builder/src/InlineBlockEntityOperations.php b/core/modules/layout_builder/src/InlineBlockEntityOperations.php index 0007b9b3baab..25717b33e6ed 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\EntityTypeManagerInterface; use Drupal\Core\Entity\RevisionableInterface; use Drupal\Core\Entity\SynchronizableInterface; -use Drupal\layout_builder\Plugin\Block\InlineBlock; use Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -171,24 +170,6 @@ public function handlePreSave(EntityInterface $entity) { $this->removeUnusedForEntityOnSave($entity); } - /** - * Gets a block ID for an inline block plugin. - * - * @param \Drupal\layout_builder\Plugin\Block\InlineBlock $block_plugin - * The inline block plugin. - * - * @return int - * The block content ID or null none available. - */ - protected function getPluginBlockId(InlineBlock $block_plugin) { - $configuration = $block_plugin->getConfiguration(); - if (!empty($configuration['block_revision_id'])) { - $revision_ids = $this->getBlockIdsForRevisionIds([$configuration['block_revision_id']]); - return array_pop($revision_ids); - } - return NULL; - } - /** * Delete the inline blocks and the usage records. * @@ -252,7 +233,7 @@ protected function saveInlineBlockComponent(EntityInterface $entity, SectionComp $plugin->saveBlockContent($new_revision, $duplicate_blocks); $post_save_configuration = $plugin->getConfiguration(); if ($duplicate_blocks || (empty($pre_save_configuration['block_revision_id']) && !empty($post_save_configuration['block_revision_id']))) { - $this->usage->addUsage($this->getPluginBlockId($plugin), $entity); + $this->usage->addUsage($post_save_configuration['block_id'], $entity); } $component->setConfiguration($post_save_configuration); } diff --git a/core/modules/layout_builder/src/Plugin/Block/InlineBlock.php b/core/modules/layout_builder/src/Plugin/Block/InlineBlock.php index a13bb8392d32..cfd853f6e9a6 100644 --- a/core/modules/layout_builder/src/Plugin/Block/InlineBlock.php +++ b/core/modules/layout_builder/src/Plugin/Block/InlineBlock.php @@ -115,6 +115,7 @@ public static function create(ContainerInterface $container, array $configuratio public function defaultConfiguration() { return [ 'view_mode' => 'full', + 'block_id' => NULL, 'block_revision_id' => NULL, 'block_serialized' => NULL, ]; @@ -289,6 +290,7 @@ public function saveBlockContent($new_revision = FALSE, $duplicate_block = FALSE $block->setNewRevision(); } $block->save(); + $this->configuration['block_id'] = $block->id(); $this->configuration['block_revision_id'] = $block->getRevisionId(); $this->configuration['block_serialized'] = NULL; } diff --git a/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockTestBase.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockTestBase.php index 9aff647e639f..c51a5896882c 100644 --- a/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockTestBase.php +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockTestBase.php @@ -112,18 +112,23 @@ protected function getLatestBlockEntityId() { /** * Removes an entity block from the layout but does not save the layout. */ - protected function removeInlineBlockFromLayout() { + protected function removeInlineBlockFromLayout($selector = NULL) { + $selector = $selector ?? static::INLINE_BLOCK_LOCATOR; $assert_session = $this->assertSession(); $page = $this->getSession()->getPage(); - $block_text = $page->find('css', static::INLINE_BLOCK_LOCATOR)->getText(); + $block_text = $page->find('css', $selector)->getText(); $this->assertNotEmpty($block_text); $assert_session->pageTextContains($block_text); - $this->clickContextualLink(static::INLINE_BLOCK_LOCATOR, 'Remove block'); + $this->clickContextualLink($selector, 'Remove block'); $assert_session->waitForElement('css', "#drupal-off-canvas input[value='Remove']"); $assert_session->assertWaitOnAjaxRequest(); + + // Output the new HTML. + $this->htmlOutput($page->getHtml()); + $page->find('css', '#drupal-off-canvas')->pressButton('Remove'); $assert_session->assertNoElementAfterWait('css', '#drupal-off-canvas'); - $assert_session->assertNoElementAfterWait('css', static::INLINE_BLOCK_LOCATOR); + $assert_session->assertNoElementAfterWait('css', $selector); $assert_session->assertWaitOnAjaxRequest(); $assert_session->pageTextNotContains($block_text); } diff --git a/core/modules/workspaces/tests/src/FunctionalJavascript/WorkspacesLayoutBuilderIntegrationTest.php b/core/modules/workspaces/tests/src/FunctionalJavascript/WorkspacesLayoutBuilderIntegrationTest.php new file mode 100644 index 000000000000..b4ed65d0961c --- /dev/null +++ b/core/modules/workspaces/tests/src/FunctionalJavascript/WorkspacesLayoutBuilderIntegrationTest.php @@ -0,0 +1,178 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\workspaces\FunctionalJavascript; + +use Drupal\Tests\layout_builder\FunctionalJavascript\InlineBlockTestBase; +use Drupal\Tests\system\Traits\OffCanvasTestTrait; +use Drupal\Tests\workspaces\Functional\WorkspaceTestUtilities; +use Drupal\workspaces\Entity\Workspace; + +/** + * Tests for layout editing in workspaces. + * + * @group layout_builder + * @group workspaces + * @group #slow + */ +class WorkspacesLayoutBuilderIntegrationTest extends InlineBlockTestBase { + + use OffCanvasTestTrait; + use WorkspaceTestUtilities; + + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'starterkit_theme'; + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'field_ui', + 'workspaces', + ]; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + $this->drupalLogin($this->drupalCreateUser([ + 'access contextual links', + 'configure any layout', + 'administer node display', + 'administer node fields', + 'create and edit custom blocks', + 'administer blocks', + 'administer content types', + 'administer workspaces', + 'view any workspace', + 'administer site configuration', + 'administer nodes', + 'bypass node access', + ])); + $this->setupWorkspaceSwitcherBlock(); + + // Enable layout builder. + $this->drupalGet(static::FIELD_UI_PREFIX . '/display/default'); + $this->submitForm([ + 'layout[enabled]' => TRUE, + 'layout[allow_custom]' => TRUE, + ], 'Save'); + $this->clickLink('Manage layout'); + $this->assertSession()->addressEquals(static::FIELD_UI_PREFIX . '/display/default/layout'); + // Add a basic block with the body field set. + $this->addInlineBlockToLayout('Block title', 'The DEFAULT block body'); + $this->assertSaveLayout(); + } + + /** + * Tests changing a layout/blocks inside a workspace. + */ + public function testBlocksInWorkspaces(): void { + $assert_session = $this->assertSession(); + $this->drupalGet('node/1'); + $assert_session->pageTextContains('The DEFAULT block body'); + $this->drupalGet('node/2'); + $assert_session->pageTextContains('The DEFAULT block body'); + + $stage = Workspace::load('stage'); + $this->switchToWorkspace($stage); + + // Confirm the block can be edited. + $this->drupalGet('node/1/layout'); + $new_block_body = 'The NEW block body'; + $this->configureInlineBlock('The DEFAULT block body', $new_block_body); + $this->assertSaveLayout(); + + $this->drupalGet('node/1'); + $assert_session->pageTextContains($new_block_body); + $assert_session->pageTextNotContains('The DEFAULT block body'); + $this->drupalGet('node/2'); + // Node 2 should use default layout. + $assert_session->pageTextContains('The DEFAULT block body'); + $assert_session->pageTextNotContains($new_block_body); + + // Switch back to the live workspace and verify that the changes are not + // visible there. + $this->switchToLive(); + $this->drupalGet('node/1'); + $assert_session->pageTextNotContains($new_block_body); + $assert_session->pageTextContains('The DEFAULT block body'); + + $this->switchToWorkspace($stage); + // Add a basic block with the body field set. + $this->drupalGet('node/1/layout'); + $second_block_body = 'The 2nd block body'; + $this->addInlineBlockToLayout('2nd Block title', $second_block_body); + $this->assertSaveLayout(); + $this->drupalGet('node/1'); + $assert_session->pageTextContains($second_block_body); + $this->drupalGet('node/2'); + // Node 2 should use default layout. + $assert_session->pageTextContains('The DEFAULT block body'); + $assert_session->pageTextNotContains($new_block_body); + $assert_session->pageTextNotContains($second_block_body); + + // Switch back to the live workspace and verify that the new added block is + // not visible there. + $this->switchToLive(); + $this->drupalGet('node/1'); + $assert_session->pageTextNotContains($second_block_body); + $assert_session->pageTextContains('The DEFAULT block body'); + + $stage->publish(); + $this->drupalGet('node/1'); + $assert_session->pageTextNotContains('The DEFAULT block body'); + $assert_session->pageTextContains($new_block_body); + $assert_session->pageTextContains($second_block_body); + } + + /** + * Tests that blocks can be deleted inside workspaces. + */ + public function testBlockDeletionInWorkspaces(): void { + $assert_session = $this->assertSession(); + + $stage = Workspace::load('stage'); + $this->switchToWorkspace($stage); + + $this->drupalGet('node/1/layout'); + $workspace_block_content = 'The WORKSPACE block body'; + $this->addInlineBlockToLayout('Workspace block title', $workspace_block_content); + $this->assertSaveLayout(); + + $this->drupalGet('node/1'); + $assert_session->pageTextContains('The DEFAULT block body'); + $assert_session->pageTextContains($workspace_block_content); + + $this->switchToLive(); + $assert_session->pageTextNotContains($workspace_block_content); + + $this->switchToWorkspace($stage); + $this->drupalGet('node/1/layout'); + $this->removeInlineBlockFromLayout(static::INLINE_BLOCK_LOCATOR . ' ~ ' . static::INLINE_BLOCK_LOCATOR); + $this->assertSaveLayout(); + $this->drupalGet('node/1'); + $assert_session->pageTextContains('The DEFAULT block body'); + $assert_session->pageTextNotContains($workspace_block_content); + + $this->drupalGet('node/1/layout'); + $this->removeInlineBlockFromLayout(); + $this->assertSaveLayout(); + $this->drupalGet('node/1'); + $assert_session->pageTextNotContains('The DEFAULT block body'); + $assert_session->pageTextNotContains($workspace_block_content); + + $this->switchToLive(); + $this->drupalGet('node/1'); + $assert_session->pageTextContains('The DEFAULT block body'); + $stage->publish(); + $this->drupalGet('node/1'); + $assert_session->pageTextNotContains('The DEFAULT block body'); + $assert_session->pageTextNotContains($workspace_block_content); + } + +} -- GitLab