Issue #2986005 by amateescu, longwave: Add the ability to mark (VBO) actions as workspace-safe

(cherry picked from commit 0a64504771)
merge-requests/7514/merge
catch 2024-04-29 23:24:41 +01:00
parent c6bb313951
commit dfcd148f25
11 changed files with 214 additions and 40 deletions

View File

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

View File

@ -0,0 +1,73 @@
<?php
namespace Drupal\Core\Entity\Form;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\workspaces\WorkspaceInformationInterface;
/**
* Provides helpers for checking whether objects in forms are workspace-safe.
*/
trait WorkspaceSafeFormTrait {
/**
* The workspace information service.
*/
protected ?WorkspaceInformationInterface $workspaceInfo = NULL;
/**
* Determines whether an entity used in a form is workspace-safe.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* An entity object.
*
* @return bool
* TRUE if the entity is workspace-safe, FALSE otherwise.
*/
protected function isWorkspaceSafeEntity(EntityInterface $entity): bool {
if (!\Drupal::hasService('workspaces.information')) {
return FALSE;
}
$is_supported = $this->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;
}
}

View File

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

View File

@ -12,4 +12,4 @@ namespace Drupal\Core\Form;
*
* @see \Drupal\Core\Form\WorkspaceDynamicSafeFormInterface
*/
interface WorkspaceSafeFormInterface extends FormInterface {}
interface WorkspaceSafeFormInterface {}

View File

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

View File

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

View File

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

View File

@ -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")]');

View File

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

View File

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

View File

@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\workspaces\Functional;
use Drupal\Tests\views\Functional\BulkFormTest;
use Drupal\workspaces\Entity\Workspace;
/**
* Tests the views bulk form in a workspace.
*
* @group views
* @group workspaces
*/
class WorkspaceViewsBulkFormTest extends BulkFormTest {
use WorkspaceTestUtilities;
/**
* {@inheritdoc}
*/
protected static $modules = ['block', 'workspaces', 'workspaces_test'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Override the user created in the parent method to add workspaces access.
$admin_user = $this->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();
}
}