Commit 71399ed9 authored by plach's avatar plach

Issue #3062486 by amateescu, pmelab, blazey, dixon_, plach, catch, webchick,...

Issue #3062486 by amateescu, pmelab, blazey, dixon_, plach, catch, webchick, jeqq, vijaycs85: Add the ability to create sub-workspaces in order to enable the dev -> stage -> live workflow for content

(cherry picked from commit f10a3971)
parent 92390915
...@@ -11,4 +11,5 @@ bundle: workspace ...@@ -11,4 +11,5 @@ bundle: workspace
mode: deploy mode: deploy
content: { } content: { }
hidden: hidden:
parent: true
uid: true uid: true
...@@ -100,6 +100,17 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ...@@ -100,6 +100,17 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
]) ])
->setDisplayConfigurable('form', TRUE); ->setDisplayConfigurable('form', TRUE);
$fields['parent'] = BaseFieldDefinition::create('entity_reference')
->setLabel(new TranslatableMarkup('Parent'))
->setDescription(new TranslatableMarkup('The parent workspace.'))
->setSetting('target_type', 'workspace')
->setReadOnly(TRUE)
->setDisplayConfigurable('form', TRUE)
->setDisplayOptions('form', [
'type' => 'options_select',
'weight' => 10,
]);
$fields['changed'] = BaseFieldDefinition::create('changed') $fields['changed'] = BaseFieldDefinition::create('changed')
->setLabel(new TranslatableMarkup('Changed')) ->setLabel(new TranslatableMarkup('Changed'))
->setDescription(new TranslatableMarkup('The time that the workspace was last edited.')) ->setDescription(new TranslatableMarkup('The time that the workspace was last edited.'))
...@@ -141,6 +152,29 @@ public function setCreatedTime($created) { ...@@ -141,6 +152,29 @@ public function setCreatedTime($created) {
return $this->set('created', (int) $created); return $this->set('created', (int) $created);
} }
/**
* {@inheritdoc}
*/
public function hasParent() {
return !$this->get('parent')->isEmpty();
}
/**
* {@inheritdoc}
*/
public static function preDelete(EntityStorageInterface $storage, array $entities) {
parent::preDelete($storage, $entities);
$workspace_tree = \Drupal::service('workspaces.repository')->loadTree();
// Ensure that workspaces that have descendants can not be deleted.
foreach ($entities as $entity) {
if (!empty($workspace_tree[$entity->id()]['descendants'])) {
throw new \InvalidArgumentException("The {$entity->label()} workspace can not be deleted because it has child workspaces.");
}
}
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
use Drupal\Core\Entity\RevisionableInterface; use Drupal\Core\Entity\RevisionableInterface;
use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\workspaces\Plugin\Validation\Constraint\EntityWorkspaceConflictConstraint;
use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\ContainerInterface;
/** /**
...@@ -270,18 +271,16 @@ public function entityFormAlter(array &$form, FormStateInterface $form_state, $f ...@@ -270,18 +271,16 @@ public function entityFormAlter(array &$form, FormStateInterface $form_state, $f
$form['#entity_builders'][] = [get_called_class(), 'entityFormEntityBuild']; $form['#entity_builders'][] = [get_called_class(), 'entityFormEntityBuild'];
} }
if ($workspace_ids = $this->workspaceAssociation->getEntityTrackingWorkspaceIds($entity)) { // Run the workspace conflict validation constraint when the entity form is
// An entity can only be edited in one workspace. // being built so we can "disable" it early and display a message to the
$workspace_id = reset($workspace_ids); // user, instead of allowing them to enter data that can never be saved.
foreach ($entity->validate()->getEntityViolations() as $violation) {
$active_workspace = $this->workspaceManager->getActiveWorkspace(); if ($violation->getConstraint() instanceof EntityWorkspaceConflictConstraint) {
if ($workspace_id && (!$active_workspace || $workspace_id !== $active_workspace->id())) { $form['#markup'] = $violation->getMessage();
$workspace = $this->entityTypeManager->getStorage('workspace')->load($workspace_id);
$form['#markup'] = $this->t('The content is being edited in the %label workspace.', ['%label' => $workspace->label()]);
$form['#access'] = FALSE; $form['#access'] = FALSE;
continue;
} }
} };
} }
/** /**
......
...@@ -68,7 +68,18 @@ public static function getSubscribedEvents() { ...@@ -68,7 +68,18 @@ public static function getSubscribedEvents() {
* {@inheritdoc} * {@inheritdoc}
*/ */
public function onEntityTypeCreate(EntityTypeInterface $entity_type) { public function onEntityTypeCreate(EntityTypeInterface $entity_type) {
// Nothing to do here. // If the entity type is supported by Workspaces, add the revision metadata
// field.
if ($this->workspaceManager->isEntityTypeSupported($entity_type)) {
$this->addRevisionMetadataField($entity_type);
}
}
/**
* {@inheritdoc}
*/
public function onFieldableEntityTypeCreate(EntityTypeInterface $entity_type, array $field_storage_definitions) {
$this->onEntityTypeCreate($entity_type);
} }
/** /**
...@@ -78,23 +89,7 @@ public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeI ...@@ -78,23 +89,7 @@ public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeI
// If the entity type is now supported by Workspaces, add the revision // If the entity type is now supported by Workspaces, add the revision
// metadata field. // metadata field.
if ($this->workspaceManager->isEntityTypeSupported($entity_type) && !$this->workspaceManager->isEntityTypeSupported($original)) { if ($this->workspaceManager->isEntityTypeSupported($entity_type) && !$this->workspaceManager->isEntityTypeSupported($original)) {
$revision_metadata_keys = $entity_type->get('revision_metadata_keys'); $this->addRevisionMetadataField($entity_type);
if (!isset($revision_metadata_keys['workspace'])) {
// Bail out if there's an existing field called 'workspace'.
if ($this->entityDefinitionUpdateManager->getFieldStorageDefinition('workspace', $entity_type->id())) {
throw new \RuntimeException("An existing 'workspace' field was found for the '{$entity_type->id()}' entity type. Set the 'workspace' revision metadata key to use a different field name and run this update function again.");
}
$revision_metadata_keys['workspace'] = 'workspace';
$entity_type->set('revision_metadata_keys', $revision_metadata_keys);
// We are only adding a revision metadata key so we don't need to go
// through the entity update process.
$this->entityLastInstalledSchemaRepository->setLastInstalledDefinition($entity_type);
}
$this->entityDefinitionUpdateManager->installFieldStorageDefinition($revision_metadata_keys['workspace'], $entity_type->id(), 'workspaces', $this->getWorkspaceFieldDefinition());
} }
// If the entity type is no longer supported by Workspaces, remove the // If the entity type is no longer supported by Workspaces, remove the
...@@ -128,6 +123,32 @@ public function onEntityTypeDelete(EntityTypeInterface $entity_type) { ...@@ -128,6 +123,32 @@ public function onEntityTypeDelete(EntityTypeInterface $entity_type) {
// Nothing to do here. // Nothing to do here.
} }
/**
* Adds the 'workspace' revision metadata field to an entity type.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type that has been installed or updated.
*/
protected function addRevisionMetadataField(EntityTypeInterface $entity_type) {
$revision_metadata_keys = $entity_type->get('revision_metadata_keys');
if (!isset($revision_metadata_keys['workspace'])) {
// Bail out if there's an existing field called 'workspace'.
if ($this->entityDefinitionUpdateManager->getFieldStorageDefinition('workspace', $entity_type->id())) {
throw new \RuntimeException("An existing 'workspace' field was found for the '{$entity_type->id()}' entity type. Set the 'workspace' revision metadata key to use a different field name and run this update function again.");
}
$revision_metadata_keys['workspace'] = 'workspace';
$entity_type->set('revision_metadata_keys', $revision_metadata_keys);
// We are only adding a revision metadata key so we don't need to go
// through the entity update process.
$this->entityLastInstalledSchemaRepository->setLastInstalledDefinition($entity_type);
}
$this->entityDefinitionUpdateManager->installFieldStorageDefinition($revision_metadata_keys['workspace'], $entity_type->id(), 'workspaces', $this->getWorkspaceFieldDefinition());
}
/** /**
* Gets the base field definition for the 'workspace' revision metadata field. * Gets the base field definition for the 'workspace' revision metadata field.
* *
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
use Drupal\Core\Entity\EntityTypeBundleInfoInterface; use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Form\FormStateInterface;
use Drupal\workspaces\WorkspaceAssociationInterface; use Drupal\workspaces\WorkspaceAssociationInterface;
use Drupal\workspaces\WorkspaceRepositoryInterface;
use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\ContainerInterface;
/** /**
...@@ -31,6 +32,13 @@ class WorkspaceDeleteForm extends ContentEntityDeleteForm implements WorkspaceFo ...@@ -31,6 +32,13 @@ class WorkspaceDeleteForm extends ContentEntityDeleteForm implements WorkspaceFo
*/ */
protected $workspaceAssociation; protected $workspaceAssociation;
/**
* The workspace repository service.
*
* @var \Drupal\workspaces\WorkspaceRepositoryInterface
*/
protected $workspaceRepository;
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
...@@ -38,6 +46,7 @@ public static function create(ContainerInterface $container) { ...@@ -38,6 +46,7 @@ public static function create(ContainerInterface $container) {
return new static( return new static(
$container->get('entity.repository'), $container->get('entity.repository'),
$container->get('workspaces.association'), $container->get('workspaces.association'),
$container->get('workspaces.repository'),
$container->get('entity_type.bundle.info'), $container->get('entity_type.bundle.info'),
$container->get('datetime.time') $container->get('datetime.time')
); );
...@@ -51,14 +60,17 @@ public static function create(ContainerInterface $container) { ...@@ -51,14 +60,17 @@ public static function create(ContainerInterface $container) {
* @param \Drupal\workspaces\WorkspaceAssociationInterface $workspace_association * @param \Drupal\workspaces\WorkspaceAssociationInterface $workspace_association
* The workspace association service to check how many revisions will be * The workspace association service to check how many revisions will be
* deleted. * deleted.
* @param \Drupal\workspaces\WorkspaceRepositoryInterface $workspace_repository
* The workspace repository service.
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
* The entity type bundle service. * The entity type bundle service.
* @param \Drupal\Component\Datetime\TimeInterface $time * @param \Drupal\Component\Datetime\TimeInterface $time
* The time service. * The time service.
*/ */
public function __construct(EntityRepositoryInterface $entity_repository, WorkspaceAssociationInterface $workspace_association, EntityTypeBundleInfoInterface $entity_type_bundle_info = NULL, TimeInterface $time = NULL) { public function __construct(EntityRepositoryInterface $entity_repository, WorkspaceAssociationInterface $workspace_association, WorkspaceRepositoryInterface $workspace_repository, EntityTypeBundleInfoInterface $entity_type_bundle_info = NULL, TimeInterface $time = NULL) {
parent::__construct($entity_repository, $entity_type_bundle_info, $time); parent::__construct($entity_repository, $entity_type_bundle_info, $time);
$this->workspaceAssociation = $workspace_association; $this->workspaceAssociation = $workspace_association;
$this->workspaceRepository = $workspace_repository;
} }
/** /**
...@@ -66,6 +78,17 @@ public function __construct(EntityRepositoryInterface $entity_repository, Worksp ...@@ -66,6 +78,17 @@ public function __construct(EntityRepositoryInterface $entity_repository, Worksp
*/ */
public function buildForm(array $form, FormStateInterface $form_state) { public function buildForm(array $form, FormStateInterface $form_state) {
$form = parent::buildForm($form, $form_state); $form = parent::buildForm($form, $form_state);
$workspace_tree = $this->workspaceRepository->loadTree();
if (!empty($workspace_tree[$this->entity->id()]['descendants'])) {
$form['description']['#markup'] = $this->t('The %label workspace can not be deleted because it has child workspaces.', [
'%label' => $this->entity->label(),
]);
$form['actions']['submit']['#disabled'] = TRUE;
return $form;
}
$tracked_entities = $this->workspaceAssociation->getTrackedEntities($this->entity->id()); $tracked_entities = $this->workspaceAssociation->getTrackedEntities($this->entity->id());
$items = []; $items = [];
foreach (array_keys($tracked_entities) as $entity_type_id => $entity_ids) { foreach (array_keys($tracked_entities) as $entity_type_id => $entity_ids) {
......
<?php
namespace Drupal\workspaces\Form;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Drupal\workspaces\WorkspaceInterface;
use Drupal\workspaces\WorkspaceOperationFactory;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a form that merges the contents for a workspace into another one.
*/
class WorkspaceMergeForm extends ConfirmFormBase implements WorkspaceFormInterface, ContainerInjectionInterface {
/**
* The source workspace entity.
*
* @var \Drupal\workspaces\WorkspaceInterface
*/
protected $sourceWorkspace;
/**
* The target workspace entity.
*
* @var \Drupal\workspaces\WorkspaceInterface
*/
protected $targetWorkspace;
/**
* The workspace operation factory.
*
* @var \Drupal\workspaces\WorkspaceOperationFactory
*/
protected $workspaceOperationFactory;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a new WorkspaceMergeForm.
*
* @param \Drupal\workspaces\WorkspaceOperationFactory $workspace_operation_factory
* The workspace operation factory service.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(WorkspaceOperationFactory $workspace_operation_factory, EntityTypeManagerInterface $entity_type_manager) {
$this->workspaceOperationFactory = $workspace_operation_factory;
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('workspaces.operation_factory'),
$container->get('entity_type.manager')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'workspace_merge_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, WorkspaceInterface $source_workspace = NULL, WorkspaceInterface $target_workspace = NULL) {
$this->sourceWorkspace = $source_workspace;
$this->targetWorkspace = $target_workspace;
$form = parent::buildForm($form, $form_state);
$workspace_merger = $this->workspaceOperationFactory->getMerger($this->sourceWorkspace, $this->targetWorkspace);
$args = [
'%source_label' => $this->sourceWorkspace->label(),
'%target_label' => $this->targetWorkspace->label(),
];
// List the changes that can be merged into the target.
if ($source_rev_diff = $workspace_merger->getDifferringRevisionIdsOnSource()) {
$total_count = $workspace_merger->getNumberOfChangesOnSource();
$form['merge'] = [
'#theme' => 'item_list',
'#title' => $this->formatPlural($total_count, 'There is @count item that can be merged from %source_label to %target_label', 'There are @count items that can be merged from %source_label to %target_label', $args),
'#items' => [],
'#total_count' => $total_count,
];
foreach ($source_rev_diff as $entity_type_id => $revision_difference) {
$form['merge']['#items'][$entity_type_id] = $this->entityTypeManager->getDefinition($entity_type_id)->getCountLabel(count($revision_difference));
}
}
// If there are no changes to merge, show an informational message.
if (!isset($form['merge'])) {
$form['description'] = [
'#markup' => $this->t('There are no changes that can be merged from %source_label to %target_label.', $args),
];
$form['actions']['submit']['#disabled'] = TRUE;
}
return $form;
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->t('Would you like to merge the contents of the %source_label workspace into %target_label?', [
'%source_label' => $this->sourceWorkspace->label(),
'%target_label' => $this->targetWorkspace->label(),
]);
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return $this->t('Merge workspace contents.');
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return Url::fromRoute('entity.workspace.collection', [], ['query' => \Drupal::destination()->getAsArray()]);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->workspaceOperationFactory->getMerger($this->sourceWorkspace, $this->targetWorkspace)->merge();
$this->messenger()->addMessage($this->t('The contents of the %source_label workspace have been merged into %target_label.', [
'%source_label' => $this->sourceWorkspace->label(),
'%target_label' => $this->targetWorkspace->label(),
]));
}
}
<?php
namespace Drupal\workspaces\Plugin\EntityReferenceSelection;
use Drupal\Component\Utility\Html;
use Drupal\Core\Entity\Plugin\EntityReferenceSelection\DefaultSelection;
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides specific access control for the workspace entity type.
*
* @EntityReferenceSelection(
* id = "default:workspace",
* label = @Translation("Workspace selection"),
* entity_types = {"workspace"},
* group = "default",
* weight = 1
* )
*/
class WorkspaceSelection extends DefaultSelection {
/**
* The workspace repository service.
*
* @var \Drupal\workspaces\WorkspaceRepositoryInterface
*/
protected $workspaceRepository;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
$instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
$instance->workspaceRepository = $container->get('workspaces.repository');
return $instance;
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'sort' => [
'field' => 'label',
'direction' => 'asc',
],
] + parent::defaultConfiguration();
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form = parent::buildConfigurationForm($form, $form_state);
// Sorting is not possible for workspaces because we always sort them by
// depth and label.
$form['sort']['#access'] = FALSE;
return $form;
}
/**
* {@inheritdoc}
*/
public function getReferenceableEntities($match = NULL, $match_operator = 'CONTAINS', $limit = 0) {
// Get all the workspace entities and sort them in tree order.
$storage = $this->entityTypeManager->getStorage('workspace');
$workspace_tree = $this->workspaceRepository->loadTree();
$entities = array_replace($workspace_tree, $storage->loadMultiple());
// If we need to restrict the list of workspaces by searching only a part of
// their label ($match) or by a number of results ($limit), the workspace
// tree would be mangled because it wouldn't contain all the tree items.
if ($match || $limit) {
$options = parent::getReferenceableEntities($match, $match_operator, $limit);
}
else {
$options = [];
foreach ($entities as $entity) {
$options[$entity->bundle()][$entity->id()] = str_repeat('-', $workspace_tree[$entity->id()]['depth']) . Html::escape($this->entityRepository->getTranslationFromContext($entity)->label());
}
}
$restricted_access_entities = [];
foreach ($options as $bundle => $bundle_options) {
foreach (array_keys($bundle_options) as $id) {
// If a user can not view a workspace, we need to prevent them from
// referencing that workspace as well as its descendants.
if (in_array($id, $restricted_access_entities) || !$entities[$id]->access('view', $this->currentUser)) {
$restricted_access_entities += $workspace_tree[$id]['descendants'];
unset($options[$bundle][$id]);
}
}
}
return $options;
}
}
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\workspaces\WorkspaceAssociationInterface; use Drupal\workspaces\WorkspaceAssociationInterface;
use Drupal\workspaces\WorkspaceManagerInterface; use Drupal\workspaces\WorkspaceManagerInterface;
use Drupal\workspaces\WorkspaceRepositoryInterface;
use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\ConstraintValidator;
...@@ -36,6 +37,13 @@ class EntityWorkspaceConflictConstraintValidator extends ConstraintValidator imp ...@@ -36,6 +37,13 @@ class EntityWorkspaceConflictConstraintValidator extends ConstraintValidator imp
*/ */
protected $workspaceAssociation; protected $workspaceAssociation;
/**
* The workspace repository service.
*
* @var \Drupal\workspaces\WorkspaceRepositoryInterface
*/
protected $workspaceRepository;
/** /**
* Constructs an EntityUntranslatableFieldsConstraintValidator object. * Constructs an EntityUntranslatableFieldsConstraintValidator object.
* *
...@@ -45,11 +53,14 @@ class EntityWorkspaceConflictConstraintValidator extends ConstraintValidator imp ...@@ -45,11 +53,14 @@ class EntityWorkspaceConflictConstraintValidator extends ConstraintValidator imp
* The workspace manager service. * The workspace manager service.
* @param \Drupal\workspaces\WorkspaceAssociationInterface $workspace_association * @param \Drupal\workspaces\WorkspaceAssociationInterface $workspace_association
* The workspace association service. * 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) { public function __construct(EntityTypeManagerInterface $entity_type_manager, WorkspaceManagerInterface $workspace_manager, WorkspaceAssociationInterface $workspace_association, WorkspaceRepositoryInterface $workspace_repository) {
$this->entityTypeManager = $entity_type_manager; $this->entityTypeManager = $entity_type_manager;
$this->workspaceManager = $workspace_manager; $this->workspaceManager = $workspace_manager;
$this->workspaceAssociation = $workspace_association; $this->workspaceAssociation = $workspace_association;
$this->workspaceRepository = $workspace_repository;
} }
/** /**
...@@ -59,7 +70,8 @@ public static function create(ContainerInterface $container) { ...@@ -59,7 +70,8 @@ public static function create(ContainerInterface $container) {
return new static( return new static(
$container->get('entity_type.manager'), $container->get('entity_type.manager'),
$container->get('workspaces.manager'), $container->get('workspaces.manager'),
$container->get('workspaces.association') $container->get('workspaces.association'),
$container->get('workspaces.repository')
); );
} }
...@@ -69,17 +81,25 @@ public static function create(ContainerInterface $container) { ...@@ -69,17 +81,25 @@ public static function create(ContainerInterface $container) {
public function validate($entity, Constraint $constraint) { public function validate($entity, Constraint $constraint) {
/** @var \Drupal\Core\Entity\EntityInterface $entity */ /** @var \Drupal\Core\Entity\EntityInterface $entity */
if (isset($entity) && !$entity->isNew()) { if (isset($entity) && !$entity->isNew()) {
$workspace_ids = $this->workspaceAssociation->getEntityTrackingWorkspaceIds($entity);
$active_workspace = $this->workspaceManager->getActiveWorkspace(); $active_workspace = $this->workspaceManager->getActiveWorkspace();
if ($workspace_ids && (!$active_workspace || !in_array($active_workspace->id(), $workspace_ids, TRUE))) { // Get the latest revision of the entity in order to check if it's being
// An entity can only be edited in one workspace. // edited in a different workspace.
$workspace_id = reset($workspace_ids); $latest_revision = $this->workspaceManager->executeOutsideWorkspace(function () use ($entity) {
$workspace = $this->entityTypeManager->getStorage('workspace')->load($workspace_id); $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());
$this->context->buildViolation($constraint->message) if (!$active_workspace || !in_array($active_workspace->id(), $descendants_and_self, TRUE)) {
->setParameter('%label', $workspace->label()) $this->context->buildViolation($constraint->message)
->addViolation(); ->setParameter('%label', $latest_revision_workspace->label())
->addViolation();
}
} }
} }
} }
......
...@@ -19,6 +19,11 @@ class WorkspaceAccessControlHandler extends EntityAccessControlHandler { ...@@ -19,6 +19,11 @@ class WorkspaceAccessControlHandler extends EntityAccessControlHandler {
*/ */
protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) { protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
/** @var \Drupal\workspaces\WorkspaceInterface $entity */ /** @var \Drupal\workspaces\WorkspaceInterface $entity */
if ($operation === 'publish' && $entity->hasParent()) {
$message = $this->t('Only top-level workspaces can be published.');
return AccessResult::forbidden((string) $message)->addCacheableDependency($entity);
}
if ($account->hasPermission('administer workspaces')) { if ($account->hasPermission('administer workspaces')) {
return AccessResult::allowed()->cachePerPermissions(); return AccessResult::allowed()->cachePerPermissions();
} }
......
...@@ -31,6 +31,13 @@ class WorkspaceAssociation implements WorkspaceAssociationInterface { ...@@ -31,6 +31,13 @@ class WorkspaceAssociation implements WorkspaceAssociationInterface {
*/ */
protected $entityTypeManager;