diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php index 43cd540b..a3db7697 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EntityReferenceItem.php @@ -314,8 +314,10 @@ public static function generateSampleValue(FieldDefinitionInterface $field_defin // entities created. if ($referenceable = $selection_handler->getReferenceableEntities(NULL, 'CONTAINS', 50)) { $group = array_rand($referenceable); - $values['target_id'] = array_rand($referenceable[$group]); - return $values; + if (!empty($referenceable[$group])) { + $values['target_id'] = array_rand($referenceable[$group]); + return $values; + } } // Attempt to create a sample entity, avoiding recursion. 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 7bc44618..54732ca9 100644 --- a/core/modules/layout_builder/config/schema/layout_builder.schema.yml +++ b/core/modules/layout_builder/config/schema/layout_builder.schema.yml @@ -60,6 +60,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/layout_builder.services.yml b/core/modules/layout_builder/layout_builder.services.yml index 4e9fc3ac..68ff1933 100644 --- a/core/modules/layout_builder/layout_builder.services.yml +++ b/core/modules/layout_builder/layout_builder.services.yml @@ -61,3 +61,8 @@ services: arguments: ['@layout_builder.tempstore_repository', '@messenger'] tags: - { name: event_subscriber } + layout_builder.workspace_safe_forms: + class: Drupal\layout_builder\EventSubscriber\WorkspaceSafeForms + arguments: ['@?workspaces.manager'] + tags: + - { name: event_subscriber } diff --git a/core/modules/layout_builder/src/EventSubscriber/WorkspaceSafeForms.php b/core/modules/layout_builder/src/EventSubscriber/WorkspaceSafeForms.php new file mode 100644 index 00000000..91b65a4f --- /dev/null +++ b/core/modules/layout_builder/src/EventSubscriber/WorkspaceSafeForms.php @@ -0,0 +1,103 @@ +workspaceManager = $workspace_manager; + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + $events = []; + if (class_exists(WorkspaceEvents::class)) { + $events[WorkspaceEvents::WORKSPACE_SAFE_FORM][] = 'onWorkspaceSafeForm'; + } + return $events; + } + + /** + * Marks the safeness of layout_builder forms when they are safe. + * + * @param \Drupal\layout_builder\EventSubscriber\WorkspaceSafeForms $event + * The layout_builder event. + */ + public function onWorkspaceSafeForm(WorkspaceSafeForms $event) { + if (!in_array($event->getFormId(), static::FORM_IDS, TRUE)) { + return; + } + + if ($section_storage = $this->getSectionStorage($event->getFormState())) { + $context_definitions = $section_storage->getContextDefinitions(); + if (!empty($context_definitions['entity'])) { + /** @var \Drupal\Core\Entity\EntityInterface $entity */ + $entity = $section_storage->getContext('entity')->getContextValue(); + if ($entity && $this->workspaceManager->isEntityTypeSupported($entity->getEntityType())) { + $event->setSafe(TRUE); + $event->stopPropagation(); + } + } + } + } + + /** + * 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 getSectionStorage(FormStateInterface $form_state) { + foreach ($form_state->getBuildInfo()['args'] as $argument) { + if ($argument instanceof SectionStorageInterface) { + return $argument; + } + } + } + +} diff --git a/core/modules/layout_builder/src/InlineBlockEntityOperations.php b/core/modules/layout_builder/src/InlineBlockEntityOperations.php index 5b11bbe6..b65196a0 100644 --- a/core/modules/layout_builder/src/InlineBlockEntityOperations.php +++ b/core/modules/layout_builder/src/InlineBlockEntityOperations.php @@ -6,7 +6,6 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Entity\RevisionableInterface; -use Drupal\layout_builder\Plugin\Block\InlineBlock; use Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -168,24 +167,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. * @@ -249,7 +230,9 @@ 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); + if ($entity->id()) { + $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 9f347174..6846ccfe 100644 --- a/core/modules/layout_builder/src/Plugin/Block/InlineBlock.php +++ b/core/modules/layout_builder/src/Plugin/Block/InlineBlock.php @@ -289,6 +289,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 293c450a..c6c53883 100644 --- a/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockTestBase.php +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/InlineBlockTestBase.php @@ -110,18 +110,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/layout_builder/tests/src/FunctionalJavascript/WorkspacesBlockTest.php b/core/modules/layout_builder/tests/src/FunctionalJavascript/WorkspacesBlockTest.php new file mode 100644 index 00000000..070c4ea7 --- /dev/null +++ b/core/modules/layout_builder/tests/src/FunctionalJavascript/WorkspacesBlockTest.php @@ -0,0 +1,173 @@ +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() { + $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() { + $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); + } + +}