diff --git a/core/lib/Drupal/Core/Entity/Form/DeleteMultipleForm.php b/core/lib/Drupal/Core/Entity/Form/DeleteMultipleForm.php index c24b539a0cd..0e8dc29780a 100644 --- a/core/lib/Drupal/Core/Entity/Form/DeleteMultipleForm.php +++ b/core/lib/Drupal/Core/Entity/Form/DeleteMultipleForm.php @@ -5,6 +5,7 @@ namespace Drupal\Core\Entity\Form; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Form\BaseFormIdInterface; use Drupal\Core\Form\ConfirmFormBase; +use Drupal\Core\Form\WorkspaceDynamicSafeFormInterface; use Drupal\Core\Messenger\MessengerInterface; use Drupal\Core\TypedData\TranslatableInterface; use Drupal\Core\Url; @@ -17,7 +18,9 @@ use Symfony\Component\DependencyInjection\ContainerInterface; /** * Provides an entities deletion confirmation form. */ -class DeleteMultipleForm extends ConfirmFormBase implements BaseFormIdInterface { +class DeleteMultipleForm extends ConfirmFormBase implements BaseFormIdInterface, WorkspaceDynamicSafeFormInterface { + + use WorkspaceSafeFormTrait; /** * The current user. @@ -320,4 +323,11 @@ class DeleteMultipleForm extends ConfirmFormBase implements BaseFormIdInterface return $this->formatPlural($count, "@count item has not been deleted because you do not have the necessary permissions.", "@count items have not been deleted because you do not have the necessary permissions."); } + /** + * {@inheritdoc} + */ + public function isWorkspaceSafeForm(array $form, FormStateInterface $form_state): bool { + return $this->isWorkspaceSafeEntityType($this->entityType); + } + } diff --git a/core/lib/Drupal/Core/Entity/Form/WorkspaceSafeFormTrait.php b/core/lib/Drupal/Core/Entity/Form/WorkspaceSafeFormTrait.php new file mode 100644 index 00000000000..12d371c1f19 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/Form/WorkspaceSafeFormTrait.php @@ -0,0 +1,73 @@ +getWorkspaceInfo()->isEntitySupported($entity); + $is_ignored = $this->getWorkspaceInfo()->isEntityIgnored($entity); + + return $is_supported || $is_ignored; + } + + /** + * Determines whether an entity type used in a form is workspace-safe. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * An entity type object. + * + * @return bool + * TRUE if the entity type is workspace-safe, FALSE otherwise. + */ + protected function isWorkspaceSafeEntityType(EntityTypeInterface $entity_type): bool { + if (!\Drupal::hasService('workspaces.information')) { + return FALSE; + } + + $is_supported = $this->getWorkspaceInfo()->isEntityTypeSupported($entity_type); + $is_ignored = $this->getWorkspaceInfo()->isEntityTypeIgnored($entity_type); + + return $is_supported || $is_ignored; + } + + /** + * 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/lib/Drupal/Core/Form/WorkspaceDynamicSafeFormInterface.php b/core/lib/Drupal/Core/Form/WorkspaceDynamicSafeFormInterface.php index 1deae10c7ec..abfca784cd3 100644 --- a/core/lib/Drupal/Core/Form/WorkspaceDynamicSafeFormInterface.php +++ b/core/lib/Drupal/Core/Form/WorkspaceDynamicSafeFormInterface.php @@ -12,7 +12,7 @@ namespace Drupal\Core\Form; * * @see \Drupal\Core\Form\WorkspaceSafeFormInterface */ -interface WorkspaceDynamicSafeFormInterface extends FormInterface { +interface WorkspaceDynamicSafeFormInterface { /** * Determines whether the form is safe to be submitted in a workspace. diff --git a/core/lib/Drupal/Core/Form/WorkspaceSafeFormInterface.php b/core/lib/Drupal/Core/Form/WorkspaceSafeFormInterface.php index b7b95b30b08..dfc1cf00e9f 100644 --- a/core/lib/Drupal/Core/Form/WorkspaceSafeFormInterface.php +++ b/core/lib/Drupal/Core/Form/WorkspaceSafeFormInterface.php @@ -12,4 +12,4 @@ namespace Drupal\Core\Form; * * @see \Drupal\Core\Form\WorkspaceDynamicSafeFormInterface */ -interface WorkspaceSafeFormInterface extends FormInterface {} +interface WorkspaceSafeFormInterface {} diff --git a/core/modules/layout_builder/src/Form/WorkspaceSafeFormTrait.php b/core/modules/layout_builder/src/Form/WorkspaceSafeFormTrait.php index 5cffd8bdf10..c065cb6235f 100644 --- a/core/modules/layout_builder/src/Form/WorkspaceSafeFormTrait.php +++ b/core/modules/layout_builder/src/Form/WorkspaceSafeFormTrait.php @@ -4,19 +4,16 @@ declare(strict_types=1); namespace Drupal\layout_builder\Form; +use Drupal\Core\Entity\Form\WorkspaceSafeFormTrait as EntityWorkspaceSafeFormTrait; 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; + use EntityWorkspaceSafeFormTrait; /** * Determines whether the current form is safe to be submitted in a workspace. @@ -35,13 +32,9 @@ trait WorkspaceSafeFormTrait { $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); + $entity = $section_storage->getContextValue('entity'); - if ($supported || $ignored) { - return TRUE; - } + return $this->isWorkspaceSafeEntity($entity); } } @@ -67,18 +60,4 @@ trait WorkspaceSafeFormTrait { 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/views/src/Form/ViewsFormMainForm.php b/core/modules/views/src/Form/ViewsFormMainForm.php index 80fb39d2b3f..8446fedcd17 100644 --- a/core/modules/views/src/Form/ViewsFormMainForm.php +++ b/core/modules/views/src/Form/ViewsFormMainForm.php @@ -6,6 +6,8 @@ use Drupal\Component\Render\MarkupInterface; use Drupal\Component\Utility\Html; use Drupal\Core\Form\FormInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Form\WorkspaceDynamicSafeFormInterface; +use Drupal\Core\Form\WorkspaceSafeFormInterface; use Drupal\Core\Security\TrustedCallbackInterface; use Drupal\views\Render\ViewsRenderPipelineMarkup; use Drupal\views\ViewExecutable; @@ -109,6 +111,13 @@ class ViewsFormMainForm implements FormInterface, TrustedCallbackInterface { $has_form = FALSE; if (method_exists($field, 'viewsForm')) { $field->viewsForm($form, $form_state); + + // Allow the views form to determine whether it's safe to be submitted + // in a workspace. + $workspace_safe = $field instanceof WorkspaceSafeFormInterface + || ($field instanceof WorkspaceDynamicSafeFormInterface && $field->isWorkspaceSafeForm($form, $form_state)); + $form_state->set('workspace_safe', $workspace_safe); + $has_form = TRUE; } diff --git a/core/modules/views/src/Plugin/views/field/BulkForm.php b/core/modules/views/src/Plugin/views/field/BulkForm.php index 45c4db982f3..efd35c42474 100644 --- a/core/modules/views/src/Plugin/views/field/BulkForm.php +++ b/core/modules/views/src/Plugin/views/field/BulkForm.php @@ -6,8 +6,10 @@ use Drupal\Core\Cache\CacheableDependencyInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Entity\EntityRepositoryInterface; +use Drupal\Core\Entity\Form\WorkspaceSafeFormTrait; use Drupal\Core\Entity\RevisionableInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Form\WorkspaceDynamicSafeFormInterface; use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\Messenger\MessengerInterface; use Drupal\Core\Routing\RedirectDestinationTrait; @@ -26,11 +28,12 @@ use Symfony\Component\DependencyInjection\ContainerInterface; * Defines an actions-based bulk operation form element. */ #[ViewsField("bulk_form")] -class BulkForm extends FieldPluginBase implements CacheableDependencyInterface { +class BulkForm extends FieldPluginBase implements CacheableDependencyInterface, WorkspaceDynamicSafeFormInterface { use RedirectDestinationTrait; use UncacheableFieldHandlerTrait; use EntityTranslationRenderTrait; + use WorkspaceSafeFormTrait; /** * The entity type manager. @@ -584,4 +587,12 @@ class BulkForm extends FieldPluginBase implements CacheableDependencyInterface { return $entity; } + /** + * {@inheritdoc} + */ + public function isWorkspaceSafeForm(array $form, FormStateInterface $form_state): bool { + $entity_type = $this->entityTypeManager->getDefinition($this->getEntityTypeId()); + return $this->isWorkspaceSafeEntityType($entity_type); + } + } diff --git a/core/modules/views/tests/src/Functional/BulkFormTest.php b/core/modules/views/tests/src/Functional/BulkFormTest.php index 075a493aa03..7f3a8a85015 100644 --- a/core/modules/views/tests/src/Functional/BulkFormTest.php +++ b/core/modules/views/tests/src/Functional/BulkFormTest.php @@ -30,6 +30,23 @@ class BulkFormTest extends BrowserTestBase { */ protected $defaultTheme = 'stark'; + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + + // Log in as a user with 'administer nodes' permission to have access to the + // bulk operation. + $this->drupalCreateContentType(['type' => 'page']); + $admin_user = $this->drupalCreateUser([ + 'administer nodes', + 'edit any page content', + 'delete any page content', + ]); + $this->drupalLogin($admin_user); + } + /** * Tests the bulk form. */ @@ -68,16 +85,6 @@ class BulkFormTest extends BrowserTestBase { $edit["node_bulk_form[$i]"] = TRUE; } - // Log in as a user with 'administer nodes' permission to have access to the - // bulk operation. - $this->drupalCreateContentType(['type' => 'page']); - $admin_user = $this->drupalCreateUser([ - 'administer nodes', - 'edit any page content', - 'delete any page content', - ]); - $this->drupalLogin($admin_user); - $this->drupalGet('test_bulk_form'); // Set all nodes to sticky and check that. @@ -162,6 +169,11 @@ class BulkFormTest extends BrowserTestBase { $edit = []; for ($i = 0; $i < 5; $i++) { $edit["node_bulk_form[$i]"] = TRUE; + + // $nodes[0] was unpublished above, so the bulk form displays only + // $nodes[1] - $nodes[9]. Remove deleted items from $nodes to prevent + // deleting them twice at the end of this test method. + unset($nodes[$i + 1]); } $edit += ['action' => 'node_delete_action']; $this->submitForm($edit, 'Apply to selected items'); @@ -185,6 +197,11 @@ class BulkFormTest extends BrowserTestBase { 'action' => 'node_delete_action', ]; $this->submitForm($edit, 'Apply to selected items'); + + // Remove deleted items from $nodes to prevent deleting them twice at the + // end of this test method. + unset($nodes[6]); + // Make sure we just return to the bulk view with no warnings. $this->assertSession()->addressEquals('test_bulk_form'); $this->assertSession()->elementNotExists('xpath', '//div[contains(@class, "messages--status")]'); @@ -201,6 +218,11 @@ class BulkFormTest extends BrowserTestBase { 'action' => 'node_delete_action', ]; $this->submitForm($edit, 'Apply to selected items'); + + // Remove deleted items from $nodes to prevent deleting them twice at the + // end of this test method. + unset($nodes[7], $nodes[8]); + // Make sure we don't show an action message while we are still on the // confirmation page. $this->assertSession()->elementNotExists('xpath', '//div[contains(@class, "messages--status")]'); diff --git a/core/modules/views_ui/src/ViewPreviewForm.php b/core/modules/views_ui/src/ViewPreviewForm.php index 82f93ee79af..9bd2d93189d 100644 --- a/core/modules/views_ui/src/ViewPreviewForm.php +++ b/core/modules/views_ui/src/ViewPreviewForm.php @@ -3,6 +3,7 @@ namespace Drupal\views_ui; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Form\WorkspaceSafeFormInterface; use Drupal\Core\Url; /** @@ -10,7 +11,7 @@ use Drupal\Core\Url; * * @internal */ -class ViewPreviewForm extends ViewFormBase { +class ViewPreviewForm extends ViewFormBase implements WorkspaceSafeFormInterface { /** * {@inheritdoc} diff --git a/core/modules/workspaces/tests/src/Functional/WorkspaceTestUtilities.php b/core/modules/workspaces/tests/src/Functional/WorkspaceTestUtilities.php index a4a2de7d608..082948728ed 100644 --- a/core/modules/workspaces/tests/src/Functional/WorkspaceTestUtilities.php +++ b/core/modules/workspaces/tests/src/Functional/WorkspaceTestUtilities.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Drupal\Tests\workspaces\Functional; use Drupal\Tests\block\Traits\BlockCreationTrait; +use Drupal\workspaces\Entity\Handler\IgnoredWorkspaceHandler; use Drupal\workspaces\Entity\Workspace; use Drupal\workspaces\WorkspaceInterface; @@ -202,4 +203,17 @@ trait WorkspaceTestUtilities { return $page->hasContent($label); } + /** + * Marks an entity type as ignored in a workspace. + * + * @param string $entity_type_id + * The entity type ID. + */ + protected function ignoreEntityType(string $entity_type_id): void { + $entity_type = clone \Drupal::entityTypeManager()->getDefinition($entity_type_id); + $entity_type->setHandlerClass('workspace', IgnoredWorkspaceHandler::class); + \Drupal::state()->set("$entity_type_id.entity_type", $entity_type); + \Drupal::entityTypeManager()->clearCachedDefinitions(); + } + } diff --git a/core/modules/workspaces/tests/src/Functional/WorkspaceViewsBulkFormTest.php b/core/modules/workspaces/tests/src/Functional/WorkspaceViewsBulkFormTest.php new file mode 100644 index 00000000000..c91d568208c --- /dev/null +++ b/core/modules/workspaces/tests/src/Functional/WorkspaceViewsBulkFormTest.php @@ -0,0 +1,55 @@ +drupalCreateUser([ + 'administer nodes', + 'administer workspaces', + 'edit any page content', + 'delete any page content', + ]); + $this->drupalLogin($admin_user); + + // Ensure that all the test methods are executed in the context of a + // workspace. + $this->setupWorkspaceSwitcherBlock(); + $stage = Workspace::load('stage'); + $this->switchToWorkspace($stage); + } + + public function testBulkForm() { + // Ignore entity types that are not being tested, in order to fully re-use + // the parent test method. + $this->ignoreEntityType('view'); + + parent::testBulkForm(); + } + +}