Commit 4ecf7cb1 authored by catch's avatar catch

Issue #2784921 by amateescu, timmillwood, pk188, plach, catch, Fabianx,...

Issue #2784921 by amateescu, timmillwood, pk188, plach, catch, Fabianx, dixon_, dawehner, borisson_, Sam152, yoroy, webchick, phenaproxima, larowlan: Add Workspaces experimental module
parent ada497ac
......@@ -167,7 +167,8 @@
"drupal/user": "self.version",
"drupal/views": "self.version",
"drupal/views_ui": "self.version",
"drupal/workflows": "self.version"
"drupal/workflows": "self.version",
"drupal/workspace": "self.version"
},
"extra": {
"merge-plugin": {
......
......@@ -8,6 +8,7 @@
use Drupal\taxonomy\Entity\Term;
use Drupal\Tests\SchemaCheckTestTrait;
use Drupal\Tests\system\Functional\Module\ModuleTestBase;
use Drupal\workspace\Entity\Workspace;
/**
* Tests the largest configuration import possible with all available modules.
......@@ -93,6 +94,10 @@ public function testInstallUninstall() {
$shortcuts = Shortcut::loadMultiple();
entity_delete_multiple('shortcut', array_keys($shortcuts));
// Delete any workspaces so the workspace module can be uninstalled.
$workspaces = Workspace::loadMultiple();
\Drupal::entityTypeManager()->getStorage('workspace')->delete($workspaces);
system_list_reset();
$all_modules = system_rebuild_module_data();
......
......@@ -4,6 +4,7 @@
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Logger\RfcLogLevel;
use Drupal\workspace\Entity\Workspace;
/**
* Install/uninstall core module and confirm table creation/deletion.
......@@ -148,6 +149,12 @@ public function testInstallUninstall() {
$this->preUninstallForum();
}
// Delete all workspaces before uninstall.
if ($name == 'workspace') {
$workspaces = Workspace::loadMultiple();
\Drupal::entityTypeManager()->getStorage('workspace')->delete($workspaces);
}
$now_installed_list = \Drupal::moduleHandler()->getModuleList();
$added_modules = array_diff(array_keys($now_installed_list), array_keys($was_installed_list));
while ($added_modules) {
......
langcode: en
status: true
dependencies:
config:
- core.entity_form_mode.workspace.deploy
module:
- workspace
id: workspace.workspace.deploy
targetEntityType: workspace
bundle: workspace
mode: deploy
content: { }
hidden:
uid: true
langcode: en
status: true
dependencies:
module:
- workspace
id: workspace.deploy
label: Deploy
targetEntityType: workspace
cache: true
/**
* @file
* Styling for Workspace module's toolbar tab.
*/
/* Tab appearance. */
.toolbar .toolbar-bar .workspace-toolbar-tab {
float: right; /* LTR */
background-color: #e09600;
}
[dir="rtl"] .toolbar .toolbar-bar .workspace-toolbar-tab {
float: left;
}
.toolbar .toolbar-bar .workspace-toolbar-tab--is-default {
background-color: #77b259;
}
.toolbar .toolbar-bar .workspace-toolbar-tab .toolbar-item {
margin: 0;
}
.toolbar .toolbar-icon-workspace:before {
background-image: url("../icons/ffffff/workspace.svg");
}
/* Manage workspaces link */
.toolbar .toolbar-tray-vertical .manage-workspaces {
text-align: right; /* LTR */
padding: 1em;
}
[dir="rtl"] .toolbar .toolbar-tray-vertical .manage-workspaces {
text-align: left;
}
.toolbar .toolbar-tray-horizontal .manage-workspaces {
float: right; /* LTR */
}
[dir="rtl"] .toolbar .toolbar-tray-horizontal .manage-workspaces {
float: left;
}
/* Individual workspace links */
.toolbar-horizontal .toolbar-tray .toolbar-menu li + li {
border-left: 1px solid #ddd; /* LTR */
}
[dir="rtl"] .toolbar-horizontal .toolbar-tray .toolbar-menu li + li {
border-left: 0 none;
border-right: 1px solid #ddd;
}
.toolbar-horizontal .toolbar-tray .toolbar-menu li:last-child {
border-right: 1px solid #ddd;
}
[dir="rtl"] .toolbar-horizontal .toolbar-tray .toolbar-menu li:last-child {
border-left: 1px solid #ddd;
}
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><g fill="none"><g fill="#FFF"><path d="M14 12L16 12 16 0 4 0 4 2 14 2 14 12ZM0 4L12 4 12 16 0 16 0 4Z"/></g></g></svg>
<?php
namespace Drupal\workspace\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines a RepositoryHandler annotation object.
*
* @see \Drupal\workspace\RepositoryHandlerInterface
* @see \Drupal\workspace\RepositoryHandlerBase
* @see \Drupal\workspace\RepositoryHandlerManager
* @see plugin_api
*
* @Annotation
*/
class RepositoryHandler extends Plugin {
/**
* The plugin ID.
*
* @var string
*/
public $id;
/**
* The human-readable name of the repository handler plugin.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $label;
/**
* A short description of the repository handler plugin.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $description;
/**
* The human-readable category.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $category = '';
}
<?php
namespace Drupal\workspace\Entity;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityChangedTrait;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\user\UserInterface;
use Drupal\workspace\WorkspaceInterface;
/**
* The workspace entity class.
*
* @ContentEntityType(
* id = "workspace",
* label = @Translation("Workspace"),
* label_collection = @Translation("Workspaces"),
* label_singular = @Translation("workspace"),
* label_plural = @Translation("workspaces"),
* label_count = @PluralTranslation(
* singular = "@count workspace",
* plural = "@count workspaces"
* ),
* handlers = {
* "list_builder" = "\Drupal\workspace\WorkspaceListBuilder",
* "access" = "Drupal\workspace\WorkspaceAccessControlHandler",
* "route_provider" = {
* "html" = "\Drupal\Core\Entity\Routing\AdminHtmlRouteProvider",
* },
* "form" = {
* "default" = "\Drupal\workspace\Form\WorkspaceForm",
* "add" = "\Drupal\workspace\Form\WorkspaceForm",
* "edit" = "\Drupal\workspace\Form\WorkspaceForm",
* "delete" = "\Drupal\workspace\Form\WorkspaceDeleteForm",
* "activate" = "\Drupal\workspace\Form\WorkspaceActivateForm",
* "deploy" = "\Drupal\workspace\Form\WorkspaceDeployForm",
* },
* },
* admin_permission = "administer workspaces",
* base_table = "workspace",
* revision_table = "workspace_revision",
* data_table = "workspace_field_data",
* revision_data_table = "workspace_field_revision",
* entity_keys = {
* "id" = "id",
* "revision" = "revision_id",
* "uuid" = "uuid",
* "label" = "label",
* "uid" = "uid",
* },
* links = {
* "add-form" = "/admin/config/workflow/workspace/add",
* "edit-form" = "/admin/config/workflow/workspace/manage/{workspace}/edit",
* "delete-form" = "/admin/config/workflow/workspace/manage/{workspace}/delete",
* "activate-form" = "/admin/config/workflow/workspace/manage/{workspace}/activate",
* "deploy-form" = "/admin/config/workflow/workspace/manage/{workspace}/deploy",
* "collection" = "/admin/config/workflow/workspace",
* },
* )
*/
class Workspace extends ContentEntityBase implements WorkspaceInterface {
use EntityChangedTrait;
/**
* {@inheritdoc}
*/
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
$fields = parent::baseFieldDefinitions($entity_type);
$fields['id'] = BaseFieldDefinition::create('string')
->setLabel(new TranslatableMarkup('Workspace ID'))
->setDescription(new TranslatableMarkup('The workspace ID.'))
->setSetting('max_length', 128)
->setRequired(TRUE)
->addConstraint('UniqueField')
->addConstraint('DeletedWorkspace')
->addPropertyConstraints('value', ['Regex' => ['pattern' => '/^[a-z0-9_]+$/']]);
$fields['label'] = BaseFieldDefinition::create('string')
->setLabel(new TranslatableMarkup('Workspace name'))
->setDescription(new TranslatableMarkup('The workspace name.'))
->setRevisionable(TRUE)
->setSetting('max_length', 128)
->setRequired(TRUE);
$fields['uid'] = BaseFieldDefinition::create('entity_reference')
->setLabel(new TranslatableMarkup('Owner'))
->setDescription(new TranslatableMarkup('The workspace owner.'))
->setRevisionable(TRUE)
->setSetting('target_type', 'user')
->setDefaultValueCallback('Drupal\workspace\Entity\Workspace::getCurrentUserId')
->setDisplayOptions('form', [
'type' => 'entity_reference_autocomplete',
'weight' => 5,
])
->setDisplayConfigurable('form', TRUE);
$fields['changed'] = BaseFieldDefinition::create('changed')
->setLabel(new TranslatableMarkup('Changed'))
->setDescription(new TranslatableMarkup('The time that the workspace was last edited.'))
->setRevisionable(TRUE);
$fields['created'] = BaseFieldDefinition::create('created')
->setLabel(new TranslatableMarkup('Created'))
->setDescription(new TranslatableMarkup('The time that the workspaces was created.'));
$fields['target'] = BaseFieldDefinition::create('string')
->setLabel(new TranslatableMarkup('Target workspace'))
->setDescription(new TranslatableMarkup('The workspace to push to and pull from.'))
->setRevisionable(TRUE)
->setRequired(TRUE)
->setDefaultValue('live');
return $fields;
}
/**
* {@inheritdoc}
*/
public function push() {
return $this->getRepositoryHandler()->push();
}
/**
* {@inheritdoc}
*/
public function pull() {
return $this->getRepositoryHandler()->pull();
}
/**
* {@inheritdoc}
*/
public function getRepositoryHandler() {
return \Drupal::service('plugin.manager.workspace.repository_handler')->createFromWorkspace($this);
}
/**
* {@inheritdoc}
*/
public function isDefaultWorkspace() {
return $this->id() === static::DEFAULT_WORKSPACE;
}
/**
* {@inheritdoc}
*/
public function getCreatedTime() {
return $this->get('created')->value;
}
/**
* {@inheritdoc}
*/
public function setCreatedTime($created) {
return $this->set('created', (int) $created);
}
/**
* {@inheritdoc}
*/
public function getOwner() {
return $this->get('uid')->entity;
}
/**
* {@inheritdoc}
*/
public function setOwner(UserInterface $account) {
return $this->set('uid', $account->id());
}
/**
* {@inheritdoc}
*/
public function getOwnerId() {
return $this->get('uid')->target_id;
}
/**
* {@inheritdoc}
*/
public function setOwnerId($uid) {
return $this->set('uid', $uid);
}
/**
* {@inheritdoc}
*/
public static function postDelete(EntityStorageInterface $storage, array $entities) {
parent::postDelete($storage, $entities);
// Add the IDs of the deleted workspaces to the list of workspaces that will
// 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);
// Trigger a batch purge to allow empty workspaces to be deleted
// immediately.
\Drupal::service('workspace.manager')->purgeDeletedWorkspacesBatch();
}
/**
* Default value callback for 'uid' base field definition.
*
* @see ::baseFieldDefinitions()
*
* @return int[]
* An array containing the ID of the current user.
*/
public static function getCurrentUserId() {
return [\Drupal::currentUser()->id()];
}
}
<?php
namespace Drupal\workspace\Entity;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Defines the Workspace association entity.
*
* @ContentEntityType(
* id = "workspace_association",
* label = @Translation("Workspace association"),
* label_collection = @Translation("Workspace associations"),
* label_singular = @Translation("workspace association"),
* label_plural = @Translation("workspace associations"),
* label_count = @PluralTranslation(
* singular = "@count workspace association",
* plural = "@count workspace associations"
* ),
* handlers = {
* "storage" = "Drupal\workspace\WorkspaceAssociationStorage"
* },
* base_table = "workspace_association",
* revision_table = "workspace_association_revision",
* internal = TRUE,
* entity_keys = {
* "id" = "id",
* "revision" = "revision_id",
* "uuid" = "uuid",
* }
* )
*
* @internal
* This entity is marked internal because it should not be used directly to
* alter the workspace an entity belongs to.
*/
class WorkspaceAssociation extends ContentEntityBase {
/**
* {@inheritdoc}
*/
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
$fields = parent::baseFieldDefinitions($entity_type);
$fields['workspace'] = BaseFieldDefinition::create('entity_reference')
->setLabel(new TranslatableMarkup('workspace'))
->setDescription(new TranslatableMarkup('The workspace of the referenced content.'))
->setSetting('target_type', 'workspace')
->setRequired(TRUE)
->setRevisionable(TRUE)
->addConstraint('workspace', []);
$fields['target_entity_type_id'] = BaseFieldDefinition::create('string')
->setLabel(new TranslatableMarkup('Content entity type ID'))
->setDescription(new TranslatableMarkup('The ID of the content entity type associated with this workspace.'))
->setSetting('max_length', EntityTypeInterface::ID_MAX_LENGTH)
->setRequired(TRUE)
->setRevisionable(TRUE);
$fields['target_entity_id'] = BaseFieldDefinition::create('integer')
->setLabel(new TranslatableMarkup('Content entity ID'))
->setDescription(new TranslatableMarkup('The ID of the content entity associated with this workspace.'))
->setRequired(TRUE)
->setRevisionable(TRUE);
$fields['target_entity_revision_id'] = BaseFieldDefinition::create('integer')
->setLabel(new TranslatableMarkup('Content entity revision ID'))
->setDescription(new TranslatableMarkup('The revision ID of the content entity associated with this workspace.'))
->setRequired(TRUE)
->setRevisionable(TRUE);
return $fields;
}
}
<?php
namespace Drupal\workspace;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Service wrapper for hooks relating to entity access control.
*
* @internal
*/
class EntityAccess implements ContainerInjectionInterface {
use StringTranslationTrait;
/**
* The entity type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The workspace manager service.
*
* @var \Drupal\workspace\WorkspaceManagerInterface
*/
protected $workspaceManager;
/**
* Constructs a new EntityAccess instance.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
* @param \Drupal\workspace\WorkspaceManagerInterface $workspace_manager
* The workspace manager service.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, WorkspaceManagerInterface $workspace_manager) {
$this->entityTypeManager = $entity_type_manager;
$this->workspaceManager = $workspace_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager'),
$container->get('workspace.manager')
);
}
/**
* Implements a hook bridge for hook_entity_access().
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to check access for.
* @param string $operation
* The operation being performed.
* @param \Drupal\Core\Session\AccountInterface $account
* The user account making the to check access for.
*
* @return \Drupal\Core\Access\AccessResult
* The result of the access check.
*
* @see hook_entity_access()
*/
public function entityOperationAccess(EntityInterface $entity, $operation, AccountInterface $account) {
// 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())) {
return AccessResult::neutral();
}
return $this->bypassAccessResult($account);
}
/**
* Implements a hook bridge for hook_entity_create_access().
*
* @param \Drupal\Core\Session\AccountInterface $account
* The user account making the to check access for.
* @param array $context
* The context of the access check.
* @param string $entity_bundle
* The bundle of the entity.
*
* @return \Drupal\Core\Access\AccessResult
* The result of the access check.
*
* @see hook_entity_create_access()
*/
public function entityCreateAccess(AccountInterface $account, array $context, $entity_bundle) {
// 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.
$entity_type = $this->entityTypeManager->getDefinition($context['entity_type_id']);
if ($entity_type->id() === 'workspace' || !$this->workspaceManager->isEntityTypeSupported($entity_type)) {
return AccessResult::neutral();
}
return $this->bypassAccessResult($account);
}
/**
* Checks the 'bypass' permissions.
*
* @param \Drupal\Core\Session\AccountInterface $account
* The user account making the to check access for.
*
* @return \Drupal\Core\Access\AccessResult
* The result of the access check.
*/
protected function bypassAccessResult(AccountInterface $account) {
// This approach assumes that the current "global" active workspace is
// correct, i.e. if you're "in" a given workspace then you get ALL THE PERMS
// to ALL THE THINGS! That's why this is a dangerous permission.
$active_workspace = $this->workspaceManager->getActiveWorkspace();
return AccessResult::allowedIf($active_workspace->getOwnerId() == $account->id())->cachePerUser()->addCacheableDependency($active_workspace)
->andIf(AccessResult::allowedIfHasPermission($account, 'bypass entity access own workspace'));
}
}
This diff is collapsed.
<?php
namespace Drupal\workspace\EntityQuery;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\Query\QueryBase;
use Drupal\Core\Entity\Query\Sql\pgsql\QueryFactory as BaseQueryFactory;
use Drupal\workspace\WorkspaceManagerInterface;
/**
* Workspace PostgreSQL specific entity query implementation.
*/
class PgsqlQueryFactory extends BaseQueryFactory {
/**
* The workspace manager.
*
* @var \Drupal\workspace\WorkspaceManagerInterface
*/
protected $workspaceManager;
/**
* Constructs a PgsqlQueryFactory object.
*
* @param \Drupal\Core\Database\Connection $connection
* The database connection used by the entity query.
* @param \Drupal\workspace\WorkspaceManagerInterface $workspace_manager
* The workspace manager.
*/
public function __construct(Connection $connection, WorkspaceManagerInterface $workspace_manager) {
$this->connection = $connection;
$this->workspaceManager = $workspace_manager;
$this->namespaces = QueryBase::getNamespaces($this);
}
/**
* {@inheritdoc}
*/
public function get(EntityTypeInterface $entity_type, $conjunction) {
$class = QueryBase::getClass($this->namespaces, 'Query');
return new $class($entity_type, $conjunction, $this->connection, $this->namespaces, $this->workspaceManager);
}
/**
* {@inheritdoc}
*/
public function getAggregate(EntityTypeInterface $entity_type, $conjunction) {
$class = QueryBase::getClass($this->namespaces, 'QueryAggregate');
return new $class($entity_type, $conjunction, $this->connection, $this->namespaces, $this->workspaceManager);
}
}
<?php
namespace Drupal\workspace\EntityQuery;
use Drupal\Core\Entity\Query\Sql\Query as BaseQuery;
/**
* Alters entity queries to use a workspace revision instead of the default one.