Issue #2935780 by amateescu, Manuel Garcia, Fabianx, Sam152, kunalkursija: Remove the concept of a 'live' default workspace

merge-requests/55/head
catch 2019-08-30 12:59:59 +01:00
parent 1b6d10debd
commit d881bd0abb
37 changed files with 475 additions and 222 deletions

View File

@ -321,8 +321,6 @@ function jsonapi_jsonapi_user_filter_access(EntityTypeInterface $entity_type, Ac
*/
function jsonapi_jsonapi_workspace_filter_access(EntityTypeInterface $entity_type, $published, $owner, AccountInterface $account) {
// @see \Drupal\workspaces\WorkspaceAccessControlHandler::checkAccess()
// \Drupal\jsonapi\Access\TemporaryQueryGuard adds the condition for
// (isDefaultWorkspace()), so this does not have to.
return ([
JSONAPI_FILTER_AMONG_ALL => AccessResult::allowedIfHasPermission($account, 'view any workspace'),
JSONAPI_FILTER_AMONG_OWN => AccessResult::allowedIfHasPermission($account, 'view own workspace'),

View File

@ -15,7 +15,6 @@ use Drupal\Core\TypedData\DataReferenceDefinitionInterface;
use Drupal\jsonapi\Query\EntityCondition;
use Drupal\jsonapi\Query\EntityConditionGroup;
use Drupal\jsonapi\Query\Filter;
use Drupal\workspaces\WorkspaceInterface;
/**
* Adds sufficient access control to collection queries.
@ -306,20 +305,6 @@ class TemporaryQueryGuard {
// @see \Drupal\user\UserAccessControlHandler::checkAccess()
$specific_condition = new EntityCondition('uid', '0', '!=');
break;
case 'workspace':
// The default workspace is always viewable, no matter what, so if
// the generic condition prevents that, add an OR.
// @see \Drupal\workspaces\WorkspaceAccessControlHandler::checkAccess()
if ($generic_condition) {
$specific_condition = new EntityConditionGroup('OR', [
$generic_condition,
new EntityCondition('id', WorkspaceInterface::DEFAULT_WORKSPACE),
]);
// The generic condition is now part of the specific condition.
$generic_condition = NULL;
}
break;
}
// Return a combined condition.

View File

@ -0,0 +1,50 @@
<?php
namespace Drupal\workspaces\Access;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\workspaces\WorkspaceManagerInterface;
use Symfony\Component\Routing\Route;
/**
* Determines access to routes based on the presence of an active workspace.
*/
class ActiveWorkspaceCheck implements AccessInterface {
/**
* The workspace manager.
*
* @var \Drupal\workspaces\WorkspaceManagerInterface
*/
protected $workspaceManager;
/**
* Constructs a new ActiveWorkspaceCheck.
*
* @param \Drupal\workspaces\WorkspaceManagerInterface $workspace_manager
* The workspace manager.
*/
public function __construct(WorkspaceManagerInterface $workspace_manager) {
$this->workspaceManager = $workspace_manager;
}
/**
* Checks access.
*
* @param \Symfony\Component\Routing\Route $route
* The route to check against.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
public function access(Route $route) {
if (!$route->hasRequirement('_has_active_workspace')) {
return AccessResult::neutral();
}
$required_value = filter_var($route->getRequirement('_has_active_workspace'), FILTER_VALIDATE_BOOLEAN);
return AccessResult::allowedIf($required_value === $this->workspaceManager->hasActiveWorkspace())->addCacheContexts(['workspace']);
}
}

View File

@ -123,7 +123,8 @@ class Workspace extends ContentEntityBase implements WorkspaceInterface {
* {@inheritdoc}
*/
public function isDefaultWorkspace() {
return $this->id() === static::DEFAULT_WORKSPACE;
@trigger_error('WorkspaceInterface::isDefaultWorkspace() is deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use \Drupal\workspaces\WorkspaceManager::hasActiveWorkspace() instead. See https://www.drupal.org/node/3071527', E_USER_DEPRECATED);
return FALSE;
}
/**
@ -150,7 +151,6 @@ class Workspace extends ContentEntityBase implements WorkspaceInterface {
// be purged on cron.
$state = \Drupal::state();
$deleted_workspace_ids = $state->get('workspace.deleted', []);
unset($entities[static::DEFAULT_WORKSPACE]);
$deleted_workspace_ids += array_combine(array_keys($entities), array_keys($entities));
$state->set('workspace.deleted', $deleted_workspace_ids);

View File

@ -75,7 +75,7 @@ class EntityAccess implements ContainerInjectionInterface {
// Workspaces themselves are handled by their own access handler and we
// should not try to do any access checks for entity types that can not
// belong to a workspace.
if ($entity->getEntityTypeId() === 'workspace' || !$this->workspaceManager->isEntityTypeSupported($entity->getEntityType())) {
if ($entity->getEntityTypeId() === 'workspace' || !$this->workspaceManager->isEntityTypeSupported($entity->getEntityType()) || !$this->workspaceManager->hasActiveWorkspace()) {
return AccessResult::neutral();
}
@ -102,7 +102,7 @@ class EntityAccess implements ContainerInjectionInterface {
// should not try to do any access checks for entity types that can not
// belong to a workspace.
$entity_type = $this->entityTypeManager->getDefinition($context['entity_type_id']);
if ($entity_type->id() === 'workspace' || !$this->workspaceManager->isEntityTypeSupported($entity_type)) {
if ($entity_type->id() === 'workspace' || !$this->workspaceManager->isEntityTypeSupported($entity_type) || !$this->workspaceManager->hasActiveWorkspace()) {
return AccessResult::neutral();
}

View File

@ -314,8 +314,7 @@ class EntityOperations implements ContainerInjectionInterface {
// \Drupal\path\Plugin\Validation\Constraint\PathAliasConstraintValidator
// know in advance (before hook_entity_presave()) that the new revision will
// be a pending one.
$active_workspace = $this->workspaceManager->getActiveWorkspace();
if (!$active_workspace->isDefaultWorkspace()) {
if ($this->workspaceManager->hasActiveWorkspace()) {
$form['#entity_builders'][] = [get_called_class(), 'entityFormEntityBuild'];
}
@ -325,7 +324,8 @@ class EntityOperations implements ContainerInjectionInterface {
// An entity can only be edited in one workspace.
$workspace_id = reset($workspace_ids);
if ($workspace_id !== $active_workspace->id()) {
$active_workspace = $this->workspaceManager->getActiveWorkspace();
if ($workspace_id && (!$active_workspace || $workspace_id !== $active_workspace->id())) {
$workspace = $this->entityTypeManager->getStorage('workspace')->load($workspace_id);
$form['#markup'] = $this->t('The content is being edited in the %label workspace.', ['%label' => $workspace->label()]);
@ -360,7 +360,7 @@ class EntityOperations implements ContainerInjectionInterface {
// - the entity type is internal, which means that it should not affect
// anything in the default (Live) workspace;
// - we are in the default workspace.
return $entity_type->getProvider() === 'workspaces' || $entity_type->isInternal() || $this->workspaceManager->getActiveWorkspace()->isDefaultWorkspace();
return $entity_type->getProvider() === 'workspaces' || $entity_type->isInternal() || !$this->workspaceManager->hasActiveWorkspace();
}
}

View File

@ -54,8 +54,8 @@ trait QueryTrait {
// Only alter the query if the active workspace is not the default one and
// the entity type is supported.
$active_workspace = $this->workspaceManager->getActiveWorkspace();
if (!$active_workspace->isDefaultWorkspace() && $this->workspaceManager->isEntityTypeSupported($this->entityType)) {
if ($this->workspaceManager->hasActiveWorkspace() && $this->workspaceManager->isEntityTypeSupported($this->entityType)) {
$active_workspace = $this->workspaceManager->getActiveWorkspace();
$this->sqlQuery->addMetaData('active_workspace_id', $active_workspace->id());
$this->sqlQuery->addMetaData('simple_query', FALSE);

View File

@ -0,0 +1,79 @@
<?php
namespace Drupal\workspaces\Form;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Drupal\workspaces\WorkspaceManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a form that switches to the live version of the site.
*/
class SwitchToLiveForm extends ConfirmFormBase implements WorkspaceFormInterface, ContainerInjectionInterface {
/**
* The workspace manager.
*
* @var \Drupal\workspaces\WorkspaceManagerInterface
*/
protected $workspaceManager;
/**
* Constructs a new SwitchToLiveForm.
*
* @param \Drupal\workspaces\WorkspaceManagerInterface $workspace_manager
* The workspace manager.
*/
public function __construct(WorkspaceManagerInterface $workspace_manager) {
$this->workspaceManager = $workspace_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('workspaces.manager')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'switch_to_live_form';
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->t('Would you like to switch to the live version of the site?');
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return $this->t('Switch to the live version of the site.');
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return new Url('<current>');
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->workspaceManager->switchToLive();
$this->messenger()->addMessage($this->t('You are now viewing the live version of the site.'));
}
}

View File

@ -81,12 +81,14 @@ class WorkspaceSwitcherForm extends FormBase implements WorkspaceFormInterface {
}
$active_workspace = $this->workspaceManager->getActiveWorkspace();
unset($workspace_labels[$active_workspace->id()]);
if ($active_workspace) {
unset($workspace_labels[$active_workspace->id()]);
}
$form['current'] = [
'#type' => 'item',
'#title' => $this->t('Current workspace'),
'#markup' => $active_workspace->label(),
'#markup' => $active_workspace ? $active_workspace->label() : $this->t('None'),
'#wrapper_attributes' => [
'class' => ['container-inline'],
],
@ -100,13 +102,27 @@ class WorkspaceSwitcherForm extends FormBase implements WorkspaceFormInterface {
'#wrapper_attributes' => [
'class' => ['container-inline'],
],
'#access' => !empty($workspace_labels),
];
$form['submit'] = [
$form['actions']['#type'] = 'actions';
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Activate'),
'#button_type' => 'primary',
'#access' => !empty($workspace_labels),
];
if ($active_workspace) {
$form['actions']['switch_to_live'] = [
'#type' => 'submit',
'#submit' => ['::submitSwitchToLive'],
'#value' => $this->t('Switch to Live'),
'#limit_validation_errors' => [],
'#button_type' => 'primary',
];
}
return $form;
}
@ -128,4 +144,12 @@ class WorkspaceSwitcherForm extends FormBase implements WorkspaceFormInterface {
}
}
/**
* Submit handler for switching to the live version of the site.
*/
public function submitSwitchToLive(array &$form, FormStateInterface $form_state) {
$this->workspaceManager->switchToLive();
$this->messenger->addMessage($this->t('You are now viewing the live version of the site.'));
}
}

View File

@ -56,8 +56,8 @@ class FormOperations implements ContainerInjectionInterface {
* @see hook_form_alter()
*/
public function formAlter(array &$form, FormStateInterface $form_state, $form_id) {
// No alterations are needed in the default workspace.
if ($this->workspaceManager->getActiveWorkspace()->isDefaultWorkspace()) {
// No alterations are needed if we're not in a workspace context.
if (!$this->workspaceManager->hasActiveWorkspace()) {
return;
}

View File

@ -1,68 +0,0 @@
<?php
namespace Drupal\workspaces\Negotiator;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\workspaces\WorkspaceInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Defines the default workspace negotiator.
*/
class DefaultWorkspaceNegotiator implements WorkspaceNegotiatorInterface {
/**
* The workspace storage handler.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $workspaceStorage;
/**
* The default workspace entity.
*
* @var \Drupal\workspaces\WorkspaceInterface
*/
protected $defaultWorkspace;
/**
* Constructor.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
$this->workspaceStorage = $entity_type_manager->getStorage('workspace');
}
/**
* {@inheritdoc}
*/
public function applies(Request $request) {
return TRUE;
}
/**
* {@inheritdoc}
*/
public function getActiveWorkspace(Request $request) {
if (!$this->defaultWorkspace) {
$default_workspace = $this->workspaceStorage->create([
'id' => WorkspaceInterface::DEFAULT_WORKSPACE,
'label' => Unicode::ucwords(WorkspaceInterface::DEFAULT_WORKSPACE),
]);
$default_workspace->enforceIsNew(FALSE);
$this->defaultWorkspace = $default_workspace;
}
return $this->defaultWorkspace;
}
/**
* {@inheritdoc}
*/
public function setActiveWorkspace(WorkspaceInterface $workspace) {}
}

View File

@ -78,4 +78,11 @@ class SessionWorkspaceNegotiator implements WorkspaceNegotiatorInterface {
$this->session->set('active_workspace_id', $workspace->id());
}
/**
* {@inheritdoc}
*/
public function unsetActiveWorkspace() {
$this->session->remove('active_workspace_id');
}
}

View File

@ -47,4 +47,9 @@ interface WorkspaceNegotiatorInterface {
*/
public function setActiveWorkspace(WorkspaceInterface $workspace);
/**
* Unsets the negotiated workspace.
*/
public function unsetActiveWorkspace();
}

View File

@ -50,7 +50,8 @@ class EntityReferenceSupportedNewEntitiesConstraintValidator extends ConstraintV
* {@inheritdoc}
*/
public function validate($value, Constraint $constraint) {
if ($this->workspaceManager->getActiveWorkspace()->isDefaultWorkspace()) {
// The validator should run only if we are in a active workspace context.
if (!$this->workspaceManager->hasActiveWorkspace()) {
return;
}

View File

@ -62,7 +62,7 @@ class EntityWorkspaceConflictConstraintValidator extends ConstraintValidator imp
$workspace_ids = $workspace_association_storage->getEntityTrackingWorkspaceIds($entity);
$active_workspace = $this->workspaceManager->getActiveWorkspace();
if ($workspace_ids && !in_array($active_workspace->id(), $workspace_ids, TRUE)) {
if ($workspace_ids && (!$active_workspace || !in_array($active_workspace->id(), $workspace_ids, TRUE))) {
// An entity can only be edited in one workspace.
$workspace_id = reset($workspace_ids);
$workspace = $this->entityTypeManager->getStorage('workspace')->load($workspace_id);

View File

@ -108,8 +108,8 @@ class ViewsQueryAlter implements ContainerInjectionInterface {
* @see hook_views_query_alter()
*/
public function alterQuery(ViewExecutable $view, QueryPluginBase $query) {
// Don't alter any views queries if we're in the default workspace.
if ($this->workspaceManager->getActiveWorkspace()->isDefaultWorkspace()) {
// Don't alter any views queries if we're not in a workspace context.
if (!$this->workspaceManager->hasActiveWorkspace()) {
return;
}

View File

@ -19,19 +19,10 @@ class WorkspaceAccessControlHandler extends EntityAccessControlHandler {
*/
protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
/** @var \Drupal\workspaces\WorkspaceInterface $entity */
if ($operation === 'delete' && $entity->isDefaultWorkspace()) {
return AccessResult::forbidden()->addCacheableDependency($entity);
}
if ($account->hasPermission('administer workspaces')) {
return AccessResult::allowed()->cachePerPermissions();
}
// The default workspace is always viewable, no matter what.
if ($operation == 'view' && $entity->isDefaultWorkspace()) {
return AccessResult::allowed()->addCacheableDependency($entity);
}
$permission_operation = $operation === 'update' ? 'edit' : $operation;
// Check if the user has permission to access all workspaces.

View File

@ -40,7 +40,7 @@ class WorkspaceCacheContext implements CacheContextInterface {
* {@inheritdoc}
*/
public function getContext() {
return $this->workspaceManager->getActiveWorkspace()->id();
return $this->workspaceManager->hasActiveWorkspace() ? $this->workspaceManager->getActiveWorkspace()->id() : 'live';
}
/**

View File

@ -13,6 +13,11 @@ interface WorkspaceInterface extends ContentEntityInterface, EntityChangedInterf
/**
* The ID of the default workspace.
*
* @deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use
* \Drupal\workspaces\WorkspaceManager::hasActiveWorkspace() instead.
*
* @see https://www.drupal.org/node/3071527
*/
const DEFAULT_WORKSPACE = 'live';
@ -26,6 +31,11 @@ interface WorkspaceInterface extends ContentEntityInterface, EntityChangedInterf
*
* @return bool
* TRUE if this workspace is the default one (e.g 'Live'), FALSE otherwise.
*
* @deprecated in drupal:8.8.0 and is removed from drupal:9.0.0. Use
* \Drupal\workspaces\WorkspaceManager::hasActiveWorkspace() instead.
*
* @see https://www.drupal.org/node/3071527
*/
public function isDefaultWorkspace();

View File

@ -75,7 +75,7 @@ class WorkspaceListBuilder extends EntityListBuilder {
$row['data'] = $row['data'] + parent::buildRow($entity);
$active_workspace = $this->workspaceManager->getActiveWorkspace();
if ($entity->id() === $active_workspace->id()) {
if ($active_workspace && $entity->id() === $active_workspace->id()) {
$row['class'] = 'active-workspace';
}
return $row;
@ -92,7 +92,7 @@ class WorkspaceListBuilder extends EntityListBuilder {
}
$active_workspace = $this->workspaceManager->getActiveWorkspace();
if ($entity->id() != $active_workspace->id()) {
if (!$active_workspace || $entity->id() != $active_workspace->id()) {
$operations['activate'] = [
'title' => $this->t('Switch to @workspace', ['@workspace' => $entity->label()]),
// Use a weight lower than the one of the 'Edit' operation because we
@ -102,30 +102,17 @@ class WorkspaceListBuilder extends EntityListBuilder {
];
}
if (!$entity->isDefaultWorkspace()) {
$operations['deploy'] = [
'title' => $this->t('Deploy content'),
// The 'Deploy' operation should be the default one for the currently
// active workspace.
'weight' => ($entity->id() == $active_workspace->id()) ? 0 : 20,
'url' => $entity->toUrl('deploy-form', ['query' => ['destination' => $entity->toUrl('collection')->toString()]]),
];
}
$operations['deploy'] = [
'title' => $this->t('Deploy content'),
// The 'Deploy' operation should be the default one for the currently
// active workspace.
'weight' => ($active_workspace && $entity->id() == $active_workspace->id()) ? 0 : 20,
'url' => $entity->toUrl('deploy-form', ['query' => ['destination' => $entity->toUrl('collection')->toString()]]),
];
return $operations;
}
/**
* {@inheritdoc}
*/
public function load() {
$entities = parent::load();
// Make the active workspace more visible by moving it first in the list.
$active_workspace = $this->workspaceManager->getActiveWorkspace();
$entities = [$active_workspace->id() => $entities[$active_workspace->id()]] + $entities;
return $entities;
}
/**
* {@inheritdoc}
*/
@ -150,34 +137,43 @@ class WorkspaceListBuilder extends EntityListBuilder {
*/
protected function offCanvasRender(array &$build) {
$active_workspace = $this->workspaceManager->getActiveWorkspace();
if ($active_workspace) {
$active_workspace_classes = [
'active-workspace--not-default',
'active-workspace--' . $active_workspace->id(),
];
}
else {
$active_workspace_classes = [
'active-workspace--default',
];
}
$collection_url = Url::fromRoute('entity.workspace.collection');
$row_count = count($build['table']['#rows']);
$build['active_workspace'] = [
'#type' => 'container',
'#weight' => -20,
'#attributes' => [
'class' => [
'active-workspace',
$active_workspace->isDefaultWorkspace() ? 'active-workspace--default' : 'active-workspace--not-default',
'active-workspace--' . $active_workspace->id(),
],
'class' => array_merge(['active-workspace'], $active_workspace_classes),
],
'label' => [
'#type' => 'label',
'#prefix' => '<div class="active-workspace__title">' . $this->t('Current workspace:') . '</div>',
'#title' => $active_workspace->label(),
'#title' => $active_workspace ? $active_workspace->label() : $this->t('Live'),
'#title_display' => '',
'#attributes' => ['class' => 'active-workspace__label'],
],
'manage' => [
'#type' => 'link',
'#title' => $this->t('Manage workspaces'),
'#url' => $active_workspace->toUrl('collection'),
'#url' => $collection_url,
'#attributes' => [
'class' => ['active-workspace__manage'],
],
],
];
if (!$active_workspace->isDefaultWorkspace()) {
if ($active_workspace) {
$build['active_workspace']['actions'] = [
'#type' => 'container',
'#weight' => 20,
@ -198,7 +194,7 @@ class WorkspaceListBuilder extends EntityListBuilder {
$build['all_workspaces'] = [
'#type' => 'link',
'#title' => $this->t('View all @count workspaces', ['@count' => $row_count]),
'#url' => $active_workspace->toUrl('collection'),
'#url' => $collection_url,
'#attributes' => [
'class' => ['all-workspaces'],
],
@ -207,15 +203,14 @@ class WorkspaceListBuilder extends EntityListBuilder {
$items = [];
$rows = array_slice($build['table']['#rows'], 0, 5, TRUE);
foreach ($rows as $id => $row) {
if ($active_workspace->id() !== $id) {
if (!$active_workspace || $active_workspace->id() !== $id) {
$url = Url::fromRoute('entity.workspace.activate_form', ['workspace' => $id], ['query' => $this->getDestinationArray()]);
$default_class = $id === WorkspaceInterface::DEFAULT_WORKSPACE ? 'workspaces__item--default' : 'workspaces__item--not-default';
$items[] = [
'#type' => 'link',
'#title' => $row['data']['label'],
'#url' => $url,
'#attributes' => [
'class' => ['use-ajax', 'workspaces__item', $default_class],
'class' => ['use-ajax', 'workspaces__item', 'workspaces__item--not-default'],
'data-dialog-type' => 'modal',
'data-dialog-options' => Json::encode([
'width' => 500,
@ -224,6 +219,23 @@ class WorkspaceListBuilder extends EntityListBuilder {
];
}
}
// Add an item for switching to Live.
if ($active_workspace) {
$items[] = [
'#type' => 'link',
'#title' => $this->t('Live'),
'#url' => Url::fromRoute('workspaces.switch_to_live', [], ['query' => $this->getDestinationArray()]),
'#attributes' => [
'class' => ['use-ajax', 'workspaces__item', 'workspaces__item--default'],
'data-dialog-type' => 'modal',
'data-dialog-options' => Json::encode([
'width' => 500,
]),
],
];
}
$build['workspaces'] = [
'#theme' => 'item_list',
'#items' => $items,

View File

@ -91,9 +91,9 @@ class WorkspaceManager implements WorkspaceManagerInterface {
protected $negotiatorIds;
/**
* The current active workspace.
* The current active workspace or FALSE if there is no active workspace.
*
* @var \Drupal\workspaces\WorkspaceInterface
* @var \Drupal\workspaces\WorkspaceInterface|false
*/
protected $activeWorkspace;
@ -160,6 +160,13 @@ class WorkspaceManager implements WorkspaceManagerInterface {
return $entity_types;
}
/**
* {@inheritdoc}
*/
public function hasActiveWorkspace() {
return $this->getActiveWorkspace() !== FALSE;
}
/**
* {@inheritdoc}
*/
@ -169,14 +176,17 @@ class WorkspaceManager implements WorkspaceManagerInterface {
foreach ($this->negotiatorIds as $negotiator_id) {
$negotiator = $this->classResolver->getInstanceFromDefinition($negotiator_id);
if ($negotiator->applies($request)) {
if ($this->activeWorkspace = $negotiator->getActiveWorkspace($request)) {
if ($active_workspace = $negotiator->getActiveWorkspace($request)) {
break;
}
}
}
// If no negotiator was able to determine the active workspace, default to
// the live version of the site.
$this->activeWorkspace = $active_workspace ?? FALSE;
}
// The default workspace negotiator always returns a valid workspace.
return $this->activeWorkspace;
}
@ -199,19 +209,35 @@ class WorkspaceManager implements WorkspaceManagerInterface {
return $this;
}
/**
* {@inheritdoc}
*/
public function switchToLive() {
$this->doSwitchWorkspace(NULL);
// Unset the active workspace on all negotiators.
foreach ($this->negotiatorIds as $negotiator_id) {
$negotiator = $this->classResolver->getInstanceFromDefinition($negotiator_id);
$negotiator->unsetActiveWorkspace();
}
return $this;
}
/**
* Switches the current workspace.
*
* @param \Drupal\workspaces\WorkspaceInterface $workspace
* The workspace to set as active.
* @param \Drupal\workspaces\WorkspaceInterface|null $workspace
* The workspace to set as active or NULL to switch out of the currently
* active workspace.
*
* @throws \Drupal\workspaces\WorkspaceAccessException
* Thrown when the current user doesn't have access to view the workspace.
*/
protected function doSwitchWorkspace(WorkspaceInterface $workspace) {
protected function doSwitchWorkspace($workspace) {
// If the current user doesn't have access to view the workspace, they
// shouldn't be allowed to switch to it.
if (!$workspace->access('view') && !$workspace->isDefaultWorkspace()) {
if ($workspace && !$workspace->access('view')) {
$this->logger->error('Denied access to view workspace %workspace_label for user %uid', [
'%workspace_label' => $workspace->label(),
'%uid' => $this->currentUser->id(),
@ -219,7 +245,7 @@ class WorkspaceManager implements WorkspaceManagerInterface {
throw new WorkspaceAccessException('The user does not have permission to view that workspace.');
}
$this->activeWorkspace = $workspace;
$this->activeWorkspace = $workspace ?: FALSE;
// Clear the static entity cache for the supported entity types.
$cache_tags_to_invalidate = array_map(function ($entity_type_id) {
@ -247,11 +273,23 @@ class WorkspaceManager implements WorkspaceManagerInterface {
return $result;
}
/**
* {@inheritdoc}
*/
public function executeOutsideWorkspace(callable $function) {
$previous_active_workspace = $this->getActiveWorkspace();
$this->doSwitchWorkspace(NULL);
$result = $function();
$this->doSwitchWorkspace($previous_active_workspace);
return $result;
}
/**
* {@inheritdoc}
*/
public function shouldAlterOperations(EntityTypeInterface $entity_type) {
return $this->isEntityTypeSupported($entity_type) && !$this->getActiveWorkspace()->isDefaultWorkspace();
return $this->isEntityTypeSupported($entity_type) && $this->hasActiveWorkspace();
}
/**

View File

@ -28,6 +28,14 @@ interface WorkspaceManagerInterface {
*/
public function getSupportedEntityTypes();
/**
* Determines whether a workspace is active in the current request.
*
* @return bool
* TRUE if a workspace is active, FALSE otherwise.
*/
public function hasActiveWorkspace();
/**
* Gets the active workspace.
*
@ -49,6 +57,13 @@ interface WorkspaceManagerInterface {
*/
public function setActiveWorkspace(WorkspaceInterface $workspace);
/**
* Unsets the active workspace via the workspace negotiators.
*
* @return $this
*/
public function switchToLive();
/**
* Executes the given callback function in the context of a workspace.
*
@ -62,6 +77,17 @@ interface WorkspaceManagerInterface {
*/
public function executeInWorkspace($workspace_id, callable $function);
/**
* Executes the given callback function without any workspace context.
*
* @param callable $function
* The callback to be executed.
*
* @return mixed
* The callable's return value.
*/
public function executeOutsideWorkspace(callable $function);
/**
* Determines whether runtime entity operations should be altered.
*

View File

@ -4,6 +4,7 @@ namespace Drupal\workspaces;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* Default implementation of the workspace publisher.
@ -12,6 +13,8 @@ use Drupal\Core\Entity\EntityTypeManagerInterface;
*/
class WorkspacePublisher implements WorkspacePublisherInterface {
use StringTranslationTrait;
/**
* The source workspace entity.
*
@ -19,13 +22,6 @@ class WorkspacePublisher implements WorkspacePublisherInterface {
*/
protected $sourceWorkspace;
/**
* The target workspace entity.
*
* @var \Drupal\workspaces\WorkspaceInterface
*/
protected $targetWorkspace;
/**
* The entity type manager.
*
@ -70,7 +66,6 @@ class WorkspacePublisher implements WorkspacePublisherInterface {
$this->workspaceAssociationStorage = $entity_type_manager->getStorage('workspace_association');
$this->workspaceManager = $workspace_manager;
$this->sourceWorkspace = $source;
$this->targetWorkspace = $this->entityTypeManager->getStorage('workspace')->load(WorkspaceInterface::DEFAULT_WORKSPACE);
}
/**
@ -85,7 +80,7 @@ class WorkspacePublisher implements WorkspacePublisherInterface {
try {
// @todo Handle the publishing of a workspace with a batch operation in
// https://www.drupal.org/node/2958752.
$this->workspaceManager->executeInWorkspace($this->targetWorkspace->id(), function () {
$this->workspaceManager->executeOutsideWorkspace(function () {
foreach ($this->getDifferringRevisionIdsOnSource() as $entity_type_id => $revision_difference) {
$entity_revisions = $this->entityTypeManager->getStorage($entity_type_id)
@ -128,7 +123,7 @@ class WorkspacePublisher implements WorkspacePublisherInterface {
* {@inheritdoc}
*/
public function getTargetLabel() {
return $this->targetWorkspace->label();
return $this->t('Live');
}
/**

View File

@ -108,7 +108,7 @@ abstract class WorkspaceResourceTestBase extends EntityResourceTestBase {
],
'revision_id' => [
[
'value' => 3,
'value' => 2,
],
],
'uid' => [

View File

@ -79,6 +79,11 @@ class WorkspaceCacheContextTest extends BrowserTestBase {
$cid_parts = array_merge($build['#cache']['keys'], $cache_contexts_manager->convertTokensToKeys($build['#cache']['contexts'])->getKeys());
$this->assertTrue(in_array('[workspace]=stage', $cid_parts, TRUE));
// Test that a cache entry is created.
$cid = implode(':', $cid_parts);
$bin = $build['#cache']['bin'];
$this->assertTrue($this->container->get('cache.' . $bin)->get($cid), 'The entity render element has been cached.');
}
}

View File

@ -3,7 +3,6 @@
namespace Drupal\Tests\workspaces\Functional;
use Drupal\Tests\BrowserTestBase;
use Drupal\workspaces\Entity\Workspace;
/**
* Tests concurrent edits in different workspaces.
@ -23,19 +22,20 @@ class WorkspaceConcurrentEditingTest extends BrowserTestBase {
* Test switching workspace via the switcher block and admin page.
*/
public function testSwitchingWorkspaces() {
// Create a test node.
$this->createContentType(['type' => 'test', 'label' => 'Test']);
$this->setupWorkspaceSwitcherBlock();
$permissions = [
'create workspace',
'edit own workspace',
'view own workspace',
'bypass entity access own workspace',
'create test content',
'edit own test content',
];
$mayer = $this->drupalCreateUser($permissions);
$this->drupalLogin($mayer);
$this->setupWorkspaceSwitcherBlock();
// Create a test node.
$this->createContentType(['type' => 'test', 'label' => 'Test']);
$test_node = $this->createNodeThroughUi('Test node', 'test');
// Check that the user can edit the node.
@ -71,10 +71,9 @@ class WorkspaceConcurrentEditingTest extends BrowserTestBase {
$this->assertCount(1, $violations);
$this->assertEquals('The content is being edited in the <em class="placeholder">Vultures</em> workspace. As a result, your changes cannot be saved.', $violations->get(0)->getMessage());
// Switch to the Live workspace and check that the user still can not edit
// the node.
$live = Workspace::load('live');
$this->switchToWorkspace($live);
// Switch to the Live version of the site and check that the user still can
// not edit the node.
$this->switchToLive();
$this->drupalGet('/node/' . $test_node->id() . '/edit');
$page = $this->getSession()->getPage();
$this->assertFalse($page->hasField('title[0][value]'));

View File

@ -199,11 +199,6 @@ class WorkspacePermissionsTest extends BrowserTestBase {
$this->drupalGet("/admin/config/workflow/workspaces/manage/{$bears->id()}/delete");
$this->assertSession()->statusCodeEquals(200);
// Check that the default workspace can not be deleted, even by a user with
// the "delete any workspace" permission.
$this->drupalGet("/admin/config/workflow/workspaces/manage/live/delete");
$this->assertSession()->statusCodeEquals(403);
}
}

View File

@ -67,14 +67,15 @@ class WorkspaceSwitcherTest extends BrowserTestBase {
public function testQueryParameterNegotiator() {
$web_assert = $this->assertSession();
// Initially the default workspace should be active.
$web_assert->elementContains('css', '.block-workspace-switcher', 'Live');
$web_assert->elementContains('css', '.block-workspace-switcher', 'None');
// When adding a query parameter the workspace will be switched.
$this->drupalGet('<front>', ['query' => ['workspace' => 'stage']]);
$current_user_url = \Drupal::currentUser()->getAccount()->toUrl();
$this->drupalGet($current_user_url, ['query' => ['workspace' => 'stage']]);
$web_assert->elementContains('css', '.block-workspace-switcher', 'Stage');
// The workspace switching via query parameter should persist.
$this->drupalGet('<front>');
$this->drupalGet($current_user_url);
$web_assert->elementContains('css', '.block-workspace-switcher', 'Stage');
// Check that WorkspaceCacheContext provides the cache context used to

View File

@ -131,14 +131,14 @@ class WorkspaceTest extends BrowserTestBase {
$this->drupalLogin($this->editor1);
$storage = \Drupal::entityTypeManager()->getStorage('workspace');
// The current live workspace entity should be revision 1.
$live_workspace = $storage->load('live');
$this->assertEquals('1', $live_workspace->getRevisionId());
// The current 'stage' workspace entity should be revision 1.
$stage_workspace = $storage->load('stage');
$this->assertEquals('1', $stage_workspace->getRevisionId());
// Re-save the live workspace via the UI to create revision 3.
$this->drupalPostForm($live_workspace->toUrl('edit-form')->toString(), [], 'Save');
$live_workspace = $storage->loadUnchanged('live');
$this->assertEquals('3', $live_workspace->getRevisionId());
// Re-save the 'stage' workspace via the UI to create revision 2.
$this->drupalPostForm($stage_workspace->toUrl('edit-form')->toString(), [], 'Save');
$stage_workspace = $storage->loadUnchanged('stage');
$this->assertEquals('2', $stage_workspace->getRevisionId());
}
/**

View File

@ -101,6 +101,19 @@ trait WorkspaceTestUtilities {
$session->pageTextContains($workspace->label() . ' is now the active workspace.');
}
/**
* Switches to the live version of the site for subsequent requests.
*
* This assumes that the switcher block has already been setup by calling
* setupWorkspaceSwitcherBlock().
*/
protected function switchToLive() {
/** @var \Drupal\Tests\WebAssert $session */
$session = $this->assertSession();
$this->drupalPostForm(NULL, [], 'Switch to Live');
$session->pageTextContains('You are now viewing the live version of the site.');
}
/**
* Creates a node by "clicking" buttons.
*

View File

@ -505,7 +505,7 @@ class WorkspaceIntegrationTest extends KernelTestBase {
$storage = $this->entityTypeManager->getStorage($entity_type_id);
// Create the entity in the default workspace.
$this->switchToWorkspace('live');
$this->workspaceManager->switchToLive();
$entity = $storage->createWithSampleValues($entity_type_id);
if ($entity_type_id === 'workspace') {
$entity->id = 'test';
@ -536,7 +536,7 @@ class WorkspaceIntegrationTest extends KernelTestBase {
$storage = $this->entityTypeManager->getStorage($entity_type_id);
// Create the entity in the default workspace.
$this->switchToWorkspace('live');
$this->workspaceManager->switchToLive();
$entity = $storage->createWithSampleValues($entity_type_id);
if ($entity_type_id === 'workspace') {
$entity->id = 'test';
@ -581,7 +581,7 @@ class WorkspaceIntegrationTest extends KernelTestBase {
$this->initializeWorkspacesModule();
// Create an entity in the default workspace.
$this->switchToWorkspace('live');
$this->workspaceManager->switchToLive();
$node = $this->createNode([
'title' => 'live node 1',
]);
@ -594,12 +594,12 @@ class WorkspaceIntegrationTest extends KernelTestBase {
$node->save();
// Switch back to the default workspace and run the baseline assertions.
$this->switchToWorkspace('live');
$this->workspaceManager->switchToLive();
$storage = $this->entityTypeManager->getStorage('node');
$this->assertEquals('live', $this->workspaceManager->getActiveWorkspace()->id());
$this->assertFalse($this->workspaceManager->hasActiveWorkspace());
$live_node = $storage->loadUnchanged($node->id());
$live_node = $storage->load($node->id());
$this->assertEquals('live node 1', $live_node->title->value);
$result = $storage->getQuery()
@ -611,7 +611,7 @@ class WorkspaceIntegrationTest extends KernelTestBase {
$this->workspaceManager->executeInWorkspace('stage', function () use ($node, $storage) {
$this->assertEquals('stage', $this->workspaceManager->getActiveWorkspace()->id());
$stage_node = $storage->loadUnchanged($node->id());
$stage_node = $storage->load($node->id());
$this->assertEquals('stage node 1', $stage_node->title->value);
$result = $storage->getQuery()
@ -622,7 +622,7 @@ class WorkspaceIntegrationTest extends KernelTestBase {
// Check that the 'stage' workspace was not persisted by the workspace
// manager.
$this->assertEquals('live', $this->workspaceManager->getActiveWorkspace()->id());
$this->assertFalse($this->workspaceManager->getActiveWorkspace());
}
/**
@ -879,7 +879,7 @@ class WorkspaceIntegrationTest extends KernelTestBase {
$this->initializeWorkspacesModule();
$node_storage = $this->entityTypeManager->getStorage('node');
$this->switchToWorkspace('live');
$this->workspaceManager->switchToLive();
$node = $node_storage->create([
'title' => 'Foo title',
// Use the body field on node as a test case because it requires dedicated
@ -895,7 +895,7 @@ class WorkspaceIntegrationTest extends KernelTestBase {
$node->save();
$this->workspaces['stage']->publish();
$this->switchToWorkspace('live');
$this->workspaceManager->switchToLive();
$reloaded = $node_storage->load($node->id());
$this->assertEquals('Bar title', $reloaded->title->value);

View File

@ -0,0 +1,73 @@
<?php
namespace Drupal\Tests\workspaces\Unit;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Cache\Context\CacheContextsManager;
use Drupal\Tests\UnitTestCase;
use Drupal\workspaces\Access\ActiveWorkspaceCheck;
use Drupal\workspaces\WorkspaceManagerInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Routing\Route;
/**
* @coversDefaultClass \Drupal\workspaces\Access\ActiveWorkspaceCheck
*
* @group workspaces
* @group Access
*/
class ActiveWorkspaceCheckTest extends UnitTestCase {
/**
* The dependency injection container.
*
* @var \Symfony\Component\DependencyInjection\ContainerBuilder
*/
protected $container;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->container = new ContainerBuilder();
$cache_contexts_manager = $this->prophesize(CacheContextsManager::class);
$cache_contexts_manager->assertValidTokens()->willReturn(TRUE);
$cache_contexts_manager->reveal();
$this->container->set('cache_contexts_manager', $cache_contexts_manager);
\Drupal::setContainer($this->container);
}
/**
* Provides data for the testAccess method.
*
* @return array
*/
public function providerTestAccess() {
return [
[[], FALSE, FALSE],
[[], TRUE, FALSE],
[['_has_active_workspace' => 'TRUE'], TRUE, TRUE, ['workspace']],
[['_has_active_workspace' => 'TRUE'], FALSE, FALSE, ['workspace']],
[['_has_active_workspace' => 'FALSE'], TRUE, FALSE, ['workspace']],
[['_has_active_workspace' => 'FALSE'], FALSE, TRUE, ['workspace']],
];
}
/**
* @covers ::access
* @dataProvider providerTestAccess
*/
public function testAccess($requirements, $has_active_workspace, $access, array $contexts = []) {
$route = new Route('', [], $requirements);
$workspace_manager = $this->prophesize(WorkspaceManagerInterface::class);
$workspace_manager->hasActiveWorkspace()->willReturn($has_active_workspace);
$access_check = new ActiveWorkspaceCheck($workspace_manager->reveal());
$access_result = AccessResult::allowedIf($access)->addCacheContexts($contexts);
$this->assertEquals($access_result, $access_check->access($route));
}
}

View File

@ -54,13 +54,7 @@ function workspaces_install() {
// Default to user ID 1 if we could not find any other administrator users.
$owner_id = !empty($result) ? reset($result) : 1;
// Create two workspaces by default, 'live' and 'stage'.
Workspace::create([
'id' => 'live',
'label' => 'Live',
'uid' => $owner_id,
])->save();
// Create a 'stage' workspace by default.
Workspace::create([
'id' => 'stage',
'label' => 'Stage',

View File

@ -11,6 +11,7 @@ use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Url;
use Drupal\views\Plugin\views\query\QueryPluginBase;
use Drupal\views\ViewExecutable;
use Drupal\workspaces\EntityAccess;
@ -173,8 +174,8 @@ function workspaces_toolbar() {
'#type' => 'toolbar_item',
'tab' => [
'#type' => 'link',
'#title' => $active_workspace->label(),
'#url' => $active_workspace->toUrl('collection', ['query' => \Drupal::destination()->getAsArray()]),
'#title' => $active_workspace ? $active_workspace->label() : t('Live'),
'#url' => Url::fromRoute('entity.workspace.collection', [], ['query' => \Drupal::destination()->getAsArray()]),
'#attributes' => [
'title' => t('Switch workspace'),
'class' => ['use-ajax', 'toolbar-icon', 'toolbar-icon-workspace'],
@ -187,7 +188,7 @@ function workspaces_toolbar() {
],
]),
],
'#cache' => ['tags' => $active_workspace->getCacheTags()],
'#cache' => ['tags' => $active_workspace ? $active_workspace->getCacheTags() : []],
],
'#wrapper_attributes' => [
'class' => ['workspaces-toolbar-tab'],
@ -198,9 +199,9 @@ function workspaces_toolbar() {
'#weight' => 500,
];
// Add a special class to the wrapper if we are in the default workspace so we
// can highlight it with a different color.
if ($active_workspace->isDefaultWorkspace()) {
// Add a special class to the wrapper if we don't have an active workspace so
// we can highlight it with a different color.
if (!$active_workspace) {
$items['workspace']['#wrapper_attributes']['class'][] = 'workspaces-toolbar-tab--is-default';
}

View File

@ -10,3 +10,12 @@
*/
function workspaces_post_update_access_clear_caches() {
}
/**
* Remove the default workspace.
*/
function workspaces_post_update_remove_default_workspace() {
if ($workspace = \Drupal::entityTypeManager()->getStorage('workspace')->load('live')) {
$workspace->delete();
}
}

View File

@ -25,3 +25,12 @@ entity.workspace.deploy_form:
_admin_route: TRUE
requirements:
_permission: 'administer workspaces'
workspaces.switch_to_live:
path: '/admin/config/workflow/workspaces/switch-to-live'
defaults:
_form: '\Drupal\workspaces\Form\SwitchToLiveForm'
_title: 'Switch to Live'
requirements:
_user_is_logged_in: 'TRUE'
_has_active_workspace: 'TRUE'

View File

@ -8,11 +8,6 @@ services:
class: Drupal\workspaces\WorkspaceOperationFactory
arguments: ['@entity_type.manager', '@database', '@workspaces.manager']
workspaces.negotiator.default:
class: Drupal\workspaces\Negotiator\DefaultWorkspaceNegotiator
arguments: ['@entity_type.manager']
tags:
- { name: workspace_negotiator, priority: 0 }
workspaces.negotiator.session:
class: Drupal\workspaces\Negotiator\SessionWorkspaceNegotiator
arguments: ['@current_user', '@session', '@entity_type.manager']
@ -24,6 +19,12 @@ services:
tags:
- { name: workspace_negotiator, priority: 100 }
access_check.workspaces.active_workspace:
class: Drupal\workspaces\Access\ActiveWorkspaceCheck
arguments: ['@workspaces.manager']
tags:
- { name: access_check, applies_to: _has_active_workspace }
cache_context.workspace:
class: Drupal\workspaces\WorkspaceCacheContext
arguments: ['@workspaces.manager']