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

merge-requests/5423/merge
catch 2024-04-11 09:44:10 +01:00
parent 6e03532191
commit bf22a79551
14 changed files with 293 additions and 24 deletions

View File

@ -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'

View File

@ -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 @@ abstract class ConfigureBlockFormBase extends FormBase implements BaseFormIdInte
$this->delta = $delta;
$this->uuid = $component->getUuid();
$this->block = $component->getPlugin();
$this->markWorkspaceSafe($form_state);
$form_state->setTemporaryValue('gathered_contexts', $this->getPopulatedContexts($section_storage));

View File

@ -32,6 +32,7 @@ class ConfigureSectionForm extends FormBase {
use LayoutBuilderContextTrait;
use LayoutBuilderHighlightTrait;
use LayoutRebuildTrait;
use WorkspaceSafeFormTrait;
/**
* The layout tempstore repository.
@ -127,6 +128,7 @@ class ConfigureSectionForm extends FormBase {
$this->delta = $delta;
$this->isUpdate = is_null($plugin_id);
$this->pluginId = $plugin_id;
$this->markWorkspaceSafe($form_state);
$section = $this->getCurrentSection();

View File

@ -17,6 +17,8 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
*/
class DiscardLayoutChangesForm extends ConfirmFormBase {
use WorkspaceSafeFormTrait;
/**
* The layout tempstore repository.
*
@ -87,6 +89,7 @@ class DiscardLayoutChangesForm extends ConfirmFormBase {
*/
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);

View File

@ -18,6 +18,8 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
*/
class LayoutBuilderDisableForm extends ConfirmFormBase {
use WorkspaceSafeFormTrait;
/**
* The layout tempstore repository.
*
@ -92,6 +94,7 @@ class LayoutBuilderDisableForm extends ConfirmFormBase {
}
$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);

View File

@ -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 @@ abstract class LayoutRebuildConfirmFormBase extends ConfirmFormBase {
$this->sectionStorage = $section_storage;
$this->delta = $delta;
$this->markWorkspaceSafe($form_state);
$form = parent::buildForm($form, $form_state);
if ($this->isAjax()) {

View File

@ -24,6 +24,7 @@ class MoveBlockForm extends FormBase {
use LayoutBuilderContextTrait;
use LayoutBuilderHighlightTrait;
use LayoutRebuildTrait;
use WorkspaceSafeFormTrait;
/**
* The section storage.
@ -117,6 +118,7 @@ class MoveBlockForm extends FormBase {
$this->delta = $delta;
$this->uuid = $uuid;
$this->region = $region;
$this->markWorkspaceSafe($form_state);
$form['#attributes']['data-layout-builder-target-highlight-id'] = $this->blockUpdateHighlightId($uuid);

View File

@ -25,6 +25,7 @@ class OverridesEntityForm extends ContentEntityForm {
use PreviewToggleTrait;
use LayoutBuilderEntityFormTrait;
use WorkspaceSafeFormTrait;
/**
* Layout tempstore repository.
@ -90,6 +91,7 @@ class OverridesEntityForm extends ContentEntityForm {
*/
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';

View File

@ -18,6 +18,8 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
*/
class RevertOverridesForm extends ConfirmFormBase {
use WorkspaceSafeFormTrait;
/**
* The layout tempstore repository.
*
@ -99,6 +101,7 @@ class RevertOverridesForm extends ConfirmFormBase {
}
$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);

View File

@ -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;
}
}

View File

@ -7,7 +7,6 @@ use Drupal\Core\Entity\EntityInterface;
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 @@ class InlineBlockEntityOperations implements ContainerInjectionInterface {
$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 @@ class InlineBlockEntityOperations implements ContainerInjectionInterface {
$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);
}

View File

@ -115,6 +115,7 @@ class InlineBlock extends BlockBase implements ContainerFactoryPluginInterface,
public function defaultConfiguration() {
return [
'view_mode' => 'full',
'block_id' => NULL,
'block_revision_id' => NULL,
'block_serialized' => NULL,
];
@ -289,6 +290,7 @@ class InlineBlock extends BlockBase implements ContainerFactoryPluginInterface,
$block->setNewRevision();
}
$block->save();
$this->configuration['block_id'] = $block->id();
$this->configuration['block_revision_id'] = $block->getRevisionId();
$this->configuration['block_serialized'] = NULL;
}

View File

@ -112,18 +112,23 @@ abstract class InlineBlockTestBase extends WebDriverTestBase {
/**
* 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);
}

View File

@ -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);
}
}