diff --git a/core/modules/workspaces/src/Plugin/Validation/Constraint/EntityWorkspaceConflictConstraint.php b/core/modules/workspaces/src/Plugin/Validation/Constraint/EntityWorkspaceConflictConstraint.php index 6f7e65a5596a2dc4605025a849d36e10920a3a0b..3b21fe55e05668604cba2b2e3d74043ee92a8ff6 100644 --- a/core/modules/workspaces/src/Plugin/Validation/Constraint/EntityWorkspaceConflictConstraint.php +++ b/core/modules/workspaces/src/Plugin/Validation/Constraint/EntityWorkspaceConflictConstraint.php @@ -21,6 +21,6 @@ class EntityWorkspaceConflictConstraint extends SymfonyConstraint { * * @var string */ - public $message = 'The content is being edited in the %label workspace. As a result, your changes cannot be saved.'; + public $message = 'The content is being edited in the @label workspace. As a result, your changes cannot be saved.'; } diff --git a/core/modules/workspaces/src/Plugin/Validation/Constraint/EntityWorkspaceConflictConstraintValidator.php b/core/modules/workspaces/src/Plugin/Validation/Constraint/EntityWorkspaceConflictConstraintValidator.php index 64ddf9238399ba209c224545ef31bf3359a6a9de..0d5ef69a7ac3bb98fa376c9e7f318e7d14b311a3 100644 --- a/core/modules/workspaces/src/Plugin/Validation/Constraint/EntityWorkspaceConflictConstraintValidator.php +++ b/core/modules/workspaces/src/Plugin/Validation/Constraint/EntityWorkspaceConflictConstraintValidator.php @@ -13,55 +13,17 @@ /** * Validates the EntityWorkspaceConflict constraint. + * + * @internal */ class EntityWorkspaceConflictConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface { - /** - * The entity type manager. - * - * @var \Drupal\Core\Entity\EntityTypeManagerInterface - */ - protected $entityTypeManager; - - /** - * The workspace manager service. - * - * @var \Drupal\workspaces\WorkspaceManagerInterface - */ - protected $workspaceManager; - - /** - * The workspace association service. - * - * @var \Drupal\workspaces\WorkspaceAssociationInterface - */ - protected $workspaceAssociation; - - /** - * The workspace repository service. - * - * @var \Drupal\workspaces\WorkspaceRepositoryInterface - */ - protected $workspaceRepository; - - /** - * Constructs an EntityUntranslatableFieldsConstraintValidator object. - * - * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager - * The entity type manager service. - * @param \Drupal\workspaces\WorkspaceManagerInterface $workspace_manager - * The workspace manager service. - * @param \Drupal\workspaces\WorkspaceAssociationInterface $workspace_association - * The workspace association service. - * @param \Drupal\workspaces\WorkspaceRepositoryInterface $workspace_repository - * The Workspace repository service. - */ - public function __construct(EntityTypeManagerInterface $entity_type_manager, WorkspaceManagerInterface $workspace_manager, WorkspaceAssociationInterface $workspace_association, WorkspaceRepositoryInterface $workspace_repository) { - $this->entityTypeManager = $entity_type_manager; - $this->workspaceManager = $workspace_manager; - $this->workspaceAssociation = $workspace_association; - $this->workspaceRepository = $workspace_repository; - } + public function __construct( + protected readonly EntityTypeManagerInterface $entityTypeManager, + protected readonly WorkspaceManagerInterface $workspaceManager, + protected readonly WorkspaceAssociationInterface $workspaceAssociation, + protected readonly WorkspaceRepositoryInterface $workspaceRepository, + ) {} /** * {@inheritdoc} @@ -83,22 +45,18 @@ public function validate($entity, Constraint $constraint) { if (isset($entity) && !$entity->isNew()) { $active_workspace = $this->workspaceManager->getActiveWorkspace(); - // Get the latest revision of the entity in order to check if it's being - // edited in a different workspace. - $latest_revision = $this->workspaceManager->executeOutsideWorkspace(function () use ($entity) { - /** @var \Drupal\Core\Entity\RevisionableStorageInterface $storage */ - $storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId()); - return $storage->loadRevision($storage->getLatestRevisionId($entity->id())); - }); - - // If the latest revision of the entity is tracked in a workspace, it can - // only be edited in that workspace or one of its descendants. - if ($latest_revision_workspace = $latest_revision->workspace->entity) { - $descendants_and_self = $this->workspaceRepository->getDescendantsAndSelf($latest_revision_workspace->id()); + // If the entity is tracked in a workspace, it can only be edited in + // that workspace or one of its descendants. + if ($tracking_workspace_ids = $this->workspaceAssociation->getEntityTrackingWorkspaceIds($entity)) { + $first_tracking_workspace_id = reset($tracking_workspace_ids); + $descendants_and_self = $this->workspaceRepository->getDescendantsAndSelf($first_tracking_workspace_id); if (!$active_workspace || !in_array($active_workspace->id(), $descendants_and_self, TRUE)) { + $workspace = $this->entityTypeManager->getStorage('workspace') + ->load($first_tracking_workspace_id); + $this->context->buildViolation($constraint->message) - ->setParameter('%label', $latest_revision_workspace->label()) + ->setParameter('@label', $workspace->label()) ->addViolation(); } } diff --git a/core/modules/workspaces/src/WorkspaceAssociation.php b/core/modules/workspaces/src/WorkspaceAssociation.php index 7dc9a420daa47648d172ff7e16447d30e3c625b9..fb69192ddb7b21806e023498b4191be1f8979046 100644 --- a/core/modules/workspaces/src/WorkspaceAssociation.php +++ b/core/modules/workspaces/src/WorkspaceAssociation.php @@ -308,7 +308,8 @@ public function getEntityTrackingWorkspaceIds(RevisionableInterface $entity) { $query = $this->database->select(static::TABLE) ->fields(static::TABLE, ['workspace']) ->condition('target_entity_type_id', $entity->getEntityTypeId()) - ->condition('target_entity_id', $entity->id()); + ->condition('target_entity_id', $entity->id()) + ->orderBy('target_entity_revision_id', 'DESC'); return $query->execute()->fetchCol(); } diff --git a/core/modules/workspaces/tests/src/Kernel/EntityWorkspaceConflictConstraintValidatorTest.php b/core/modules/workspaces/tests/src/Kernel/EntityWorkspaceConflictConstraintValidatorTest.php new file mode 100644 index 0000000000000000000000000000000000000000..7a7154c42b6bba3683e246f3b070a725c5468447 --- /dev/null +++ b/core/modules/workspaces/tests/src/Kernel/EntityWorkspaceConflictConstraintValidatorTest.php @@ -0,0 +1,148 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\Tests\workspaces\Kernel; + +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\entity_test\Entity\EntityTestMulRevPub; +use Drupal\KernelTests\KernelTestBase; +use Drupal\Tests\user\Traits\UserCreationTrait; +use Drupal\workspaces\Entity\Workspace; + +/** + * @coversDefaultClass \Drupal\workspaces\Plugin\Validation\Constraint\EntityWorkspaceConflictConstraintValidator + * @group workspaces + */ +class EntityWorkspaceConflictConstraintValidatorTest extends KernelTestBase { + + use UserCreationTrait; + use WorkspaceTestTrait; + + /** + * {@inheritdoc} + */ + protected static $modules = [ + 'entity_test', + 'path_alias', + 'system', + 'user', + 'workspaces', + ]; + + /** + * The entity type manager. + */ + protected EntityTypeManagerInterface $entityTypeManager; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + + $this->entityTypeManager = \Drupal::entityTypeManager(); + + $this->installSchema('workspaces', ['workspace_association']); + + $this->installEntitySchema('entity_test_mulrevpub'); + $this->installEntitySchema('workspace'); + $this->installEntitySchema('user'); + $this->createUser(); + } + + /** + * @covers ::validate + */ + public function testNewEntitiesAllowedInDefaultWorkspace(): void { + // Create two top-level workspaces and a second-level one. + $stage = Workspace::create(['id' => 'stage', 'label' => 'Stage']); + $stage->save(); + $dev = Workspace::create(['id' => 'dev', 'label' => 'Dev', 'parent' => 'stage']); + $dev->save(); + $other = Workspace::create(['id' => 'other', 'label' => 'Other']); + $other->save(); + + // Create an entity in Live, and check that the validation is skipped. + $entity = EntityTestMulRevPub::create(); + $this->assertCount(0, $entity->validate()); + $entity->save(); + $entity = $this->reloadEntity($entity); + $this->assertCount(0, $entity->validate()); + + // Edit the entity in Stage. + $this->switchToWorkspace('stage'); + $entity->save(); + $entity = $this->reloadEntity($entity); + $this->assertCount(0, $entity->validate()); + + $expected_message = 'The content is being edited in the Stage workspace. As a result, your changes cannot be saved.'; + + // Check that the entity can no longer be edited in Live. + $this->switchToLive(); + $entity = $this->reloadEntity($entity); + $violations = $entity->validate(); + $this->assertCount(1, $violations); + $this->assertSame($expected_message, (string) $violations->get(0)->getMessage()); + + // Check that the entity can no longer be edited in another top-level + // workspace. + $this->switchToWorkspace('other'); + $entity = $this->reloadEntity($entity); + $violations = $entity->validate(); + $this->assertCount(1, $violations); + $this->assertSame($expected_message, (string) $violations->get(0)->getMessage()); + + // Check that the entity can still be edited in a sub-workspace of Stage. + $this->switchToWorkspace('dev'); + $entity = $this->reloadEntity($entity); + $this->assertCount(0, $entity->validate()); + + // Edit the entity in Dev. + $this->switchToWorkspace('dev'); + $entity->save(); + $entity = $this->reloadEntity($entity); + $this->assertCount(0, $entity->validate()); + + $expected_message = 'The content is being edited in the Dev workspace. As a result, your changes cannot be saved.'; + + // Check that the entity can no longer be edited in Live. + $this->switchToLive(); + $entity = $this->reloadEntity($entity); + $violations = $entity->validate(); + $this->assertCount(1, $violations); + $this->assertSame($expected_message, (string) $violations->get(0)->getMessage()); + + // Check that the entity can no longer be edited in the parent workspace. + $this->switchToWorkspace('stage'); + $entity = $this->reloadEntity($entity); + $violations = $entity->validate(); + $this->assertCount(1, $violations); + $this->assertSame($expected_message, (string) $violations->get(0)->getMessage()); + + // Check that the entity can no longer be edited in another top-level + // workspace. + $this->switchToWorkspace('other'); + $entity = $this->reloadEntity($entity); + $violations = $entity->validate(); + $this->assertCount(1, $violations); + $this->assertSame($expected_message, (string) $violations->get(0)->getMessage()); + } + + /** + * Reloads the given entity from the storage and returns it. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity to be reloaded. + * + * @return \Drupal\Core\Entity\EntityInterface + * The reloaded entity. + */ + protected function reloadEntity(EntityInterface $entity): EntityInterface { + $storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId()); + $storage->resetCache([$entity->id()]); + return $storage->load($entity->id()); + } + +}