first commit
This commit is contained in:
+15
@@ -0,0 +1,15 @@
|
||||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
config:
|
||||
- core.entity_form_mode.workspace.deploy
|
||||
module:
|
||||
- workspaces
|
||||
id: workspace.workspace.deploy
|
||||
targetEntityType: workspace
|
||||
bundle: workspace
|
||||
mode: deploy
|
||||
content: { }
|
||||
hidden:
|
||||
parent: true
|
||||
uid: true
|
||||
@@ -0,0 +1,9 @@
|
||||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
module:
|
||||
- workspaces
|
||||
id: workspace.deploy
|
||||
label: Deploy
|
||||
targetEntityType: workspace
|
||||
cache: true
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* @file
|
||||
* Styling for the Workspaces overview table.
|
||||
*/
|
||||
|
||||
/** @todo Move to Seven theme before Workspaces is marked stable. */
|
||||
tr.active-workspace {
|
||||
background-color: #ebeae4;
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
/**
|
||||
* @file
|
||||
* Styling for Workspaces module's toolbar tab.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @todo Remove this after https://www.drupal.org/project/drupal/issues/2986056
|
||||
* has been solved.
|
||||
*/
|
||||
.workspaces-dialog #drupal-off-canvas * {
|
||||
background: initial;
|
||||
}
|
||||
.workspaces-dialog #drupal-off-canvas {
|
||||
background: #444;
|
||||
}
|
||||
|
||||
/* Tab appearance. */
|
||||
.toolbar .toolbar-bar .workspaces-toolbar-tab {
|
||||
color: #000;
|
||||
background-color: #e09600;
|
||||
}
|
||||
.toolbar .toolbar-bar .workspaces-toolbar-tab--is-default {
|
||||
background-color: #81c071;
|
||||
}
|
||||
.toolbar-oriented .toolbar-bar .workspaces-toolbar-tab {
|
||||
float: right; /* LTR */
|
||||
}
|
||||
[dir="rtl"] .toolbar-oriented .toolbar-bar .workspaces-toolbar-tab {
|
||||
float: left;
|
||||
}
|
||||
|
||||
@media (min-width: 16.5em) {
|
||||
.toolbar:not(.toolbar-oriented) .toolbar-bar .workspaces-toolbar-tab {
|
||||
float: right;
|
||||
}
|
||||
[dir="rtl"] .toolbar:not(.toolbar-oriented) .toolbar-bar .workspaces-toolbar-tab {
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar .toolbar-bar .workspaces-toolbar-tab .toolbar-item {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
text-align: left;
|
||||
color: inherit;
|
||||
}
|
||||
[dir="rtl"] .toolbar .toolbar-bar .workspaces-toolbar-tab .toolbar-item {
|
||||
text-align: right;
|
||||
}
|
||||
.toolbar-oriented .toolbar-bar .workspaces-toolbar-tab .toolbar-item {
|
||||
width: auto;
|
||||
text-align: initial;
|
||||
}
|
||||
|
||||
.toolbar .toolbar-icon-workspace:before {
|
||||
background-image: url("../icons/000000/workspaces.svg");
|
||||
}
|
||||
|
||||
/* Off canvas dialog */
|
||||
.workspaces-dialog.ui-dialog-off-canvas a:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.workspaces-dialog.ui-dialog-off-canvas #drupal-off-canvas,
|
||||
.workspaces-dialog.ui-dialog-off-canvas {
|
||||
padding: 0;
|
||||
background: #333;
|
||||
}
|
||||
|
||||
.workspaces-dialog.ui-widget.ui-widget-content {
|
||||
height: 100% !important;
|
||||
border: 0;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.workspaces-dialog.ui-dialog-off-canvas .ui-dialog-titlebar {
|
||||
visibility: hidden;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
.workspaces-dialog.ui-dialog-off-canvas .ui-dialog-titlebar-close {
|
||||
top: 1em;
|
||||
right: 0.5em;
|
||||
}
|
||||
|
||||
.workspaces-dialog.ui-dialog-off-canvas .ui-dialog-titlebar .ui-button {
|
||||
z-index: 101;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
#drupal-off-canvas .active-workspace {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: 70px;
|
||||
padding: 20px;
|
||||
background-color: #444;
|
||||
}
|
||||
|
||||
#drupal-off-canvas .active-workspace__title {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#drupal-off-canvas .active-workspace__label {
|
||||
padding: 0.9em 3.2rem 1em;
|
||||
}
|
||||
|
||||
#drupal-off-canvas .active-workspace__label span {
|
||||
color: #fff;
|
||||
font-size: 1.285em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#drupal-off-canvas .active-workspace__manage {
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
outline-color: currentColor;
|
||||
font-size: 0.9286em;
|
||||
}
|
||||
|
||||
#drupal-off-canvas .active-workspace__button {
|
||||
padding: 5px 22px;
|
||||
color: #fff;
|
||||
border: solid 1px #1e5c90;
|
||||
border-radius: 20px;
|
||||
background-image: linear-gradient(to bottom, #007bc6, #0071b8);
|
||||
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.5);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#drupal-off-canvas .active-workspace__button:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#drupal-off-canvas .all-workspaces {
|
||||
display: inline-block;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
#drupal-off-canvas .workspaces__list h3 {
|
||||
padding-left: 20px;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
[dir="rtl"] #drupal-off-canvas .workspaces__list h3 {
|
||||
padding: 0 20px 0;
|
||||
}
|
||||
|
||||
#drupal-off-canvas .workspaces ul {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#drupal-off-canvas .workspaces li {
|
||||
flex: 1;
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
#drupal-off-canvas .workspaces__item {
|
||||
position: relative;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
height: 73px;
|
||||
margin-right: 1px;
|
||||
padding: 20px 0 0 50px;
|
||||
color: #fff;
|
||||
background-color: #555;
|
||||
font-size: 0.929em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
[dir=rtl] #drupal-off-canvas .workspaces__item {
|
||||
padding: 20px 50px 0 0;
|
||||
}
|
||||
|
||||
#drupal-off-canvas .workspaces__item:hover,
|
||||
#drupal-off-canvas .workspaces__item:focus {
|
||||
background-color: #666;
|
||||
}
|
||||
|
||||
#drupal-off-canvas .active-workspace__label:before,
|
||||
#drupal-off-canvas .workspaces__item:before {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
display: block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
content: "";
|
||||
background: url("../icons/f0a100/ws_icon.svg") center center no-repeat;
|
||||
background-size: 100% auto;
|
||||
}
|
||||
|
||||
#drupal-off-canvas .active-workspace--default .active-workspace__label:before,
|
||||
#drupal-off-canvas .workspaces__item--default:before {
|
||||
background-image: url("../icons/81c071/ws_icon.svg");
|
||||
}
|
||||
|
||||
[dir=rtl] #drupal-off-canvas .active-workspace__label:before,
|
||||
[dir=rtl] #drupal-off-canvas .workspaces__item:before {
|
||||
right: 20px;
|
||||
left: initial;
|
||||
}
|
||||
|
||||
#drupal-off-canvas .active-workspace__label:before {
|
||||
left: 20px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
@media all and (max-width: 766px) {
|
||||
.toolbar-oriented .toolbar-bar .workspaces-toolbar-tab .toolbar-icon-workspace {
|
||||
width: auto;
|
||||
max-width: 8em;
|
||||
}
|
||||
|
||||
.toolbar .toolbar-bar .workspaces-toolbar-tab .toolbar-icon-workspace {
|
||||
overflow: hidden;
|
||||
padding-right: 1.3333em;
|
||||
padding-left: 2.75em; /* LTR */
|
||||
white-space: nowrap;
|
||||
text-indent: 0;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
[dir=rtl] .toolbar .toolbar-bar .workspaces-toolbar-tab .toolbar-icon-workspace {
|
||||
padding-right: 2.75em;
|
||||
padding-left: 1.3333em;
|
||||
}
|
||||
|
||||
.toolbar .toolbar-bar .workspaces-toolbar-tab .toolbar-icon-workspace:before {
|
||||
left: 0.6667em; /* LTR */
|
||||
width: 20px;
|
||||
background-size: 100% auto;
|
||||
}
|
||||
[dir=rtl] .toolbar .toolbar-bar .workspaces-toolbar-tab .toolbar-icon-workspace:before {
|
||||
right: 0.6667em;
|
||||
left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 767px) {
|
||||
.workspaces-dialog.ui-widget.ui-widget-content {
|
||||
height: 161px !important;
|
||||
}
|
||||
|
||||
/* The extra div is added by Drupal.theme.ajaxWrapperMultipleRootElements
|
||||
* because we are inserting multiple root elements. */
|
||||
.workspaces-dialog #drupal-off-canvas > div {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
#drupal-off-canvas .active-workspace {
|
||||
flex-basis: 200px;
|
||||
flex-grow: 2;
|
||||
box-sizing: content-box;
|
||||
min-height: 140px;
|
||||
padding: 20px 40px 0;
|
||||
}
|
||||
|
||||
#drupal-off-canvas .active-workspace__label:before {
|
||||
left: 40px;
|
||||
}
|
||||
|
||||
[dir=rtl] #drupal-off-canvas .active-workspace__label:before {
|
||||
right: 40px;
|
||||
left: initial;
|
||||
}
|
||||
|
||||
#drupal-off-canvas .workspaces {
|
||||
position: relative;
|
||||
flex-grow: 8;
|
||||
}
|
||||
|
||||
#drupal-off-canvas .all-workspaces {
|
||||
position: absolute;
|
||||
top: 9px;
|
||||
right: 20px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
[dir=rtl] #drupal-off-canvas .all-workspaces {
|
||||
right: initial;
|
||||
left: 20px;
|
||||
}
|
||||
|
||||
#drupal-off-canvas .workspaces ul {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#drupal-off-canvas .workspaces li {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path d="M14,12 L16,12 L16,0 L4,0 L4,2 L14,2 L14,12 Z M0,4 L12,4 L12,16 L0,16 L0,4 Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 181 B |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
|
||||
<path fill="#81C071" fill-rule="evenodd" d="M19,0 L1,0 C0.449,0 0,0.448 0,1 L0,19 C0,19.552 0.45,20 1,20 L19,20 C19.552,20 20,19.55 20,19 L20,1 C20,0.44771525 19.5522847,3.38176876e-17 19,0 Z M17.001,2 C17.553,2 18.001,2.45 18.001,3 C18.001,3.55 17.551,3.999 17.001,3.999 C16.451,3.999 16.001,3.549 16.001,2.999 C16.001,2.44671525 16.4487153,1.999 17.001,1.999 L17.001,2 Z M13.001,2 C13.552,2 14.001,2.45 14.001,3 C14.001,3.55 13.551,3.999 13.001,3.999 C12.4487153,3.999 12.001,3.55128475 12.001,2.999 C12.001,2.44671525 12.4487153,1.999 13.001,1.999 L13.001,2 Z M9.001,2 C9.552,2 10.001,2.45 10.001,3 C10.001,3.55 9.551,3.999 9.001,3.999 C8.44871525,3.999 8.001,3.55128475 8.001,2.999 C8.001,2.44671525 8.44871525,1.999 9.001,1.999 L9.001,2 Z M18.001,18 L2,18 L2,6 L18.001,6 L18.001,18 Z M4.402,16 L7.598,16 C7.70460623,16.0005334 7.80701477,15.9584887 7.88249152,15.8831997 C7.95796827,15.8079107 8.00026785,15.7056072 8,15.599 L8,8.402 C8.00026565,8.29574025 7.95824022,8.19374159 7.88319685,8.11851062 C7.80815349,8.04327965 7.70626008,8.00099967 7.6,8.001 L4.396,8.001 C4.28956674,8.00073358 4.18741595,8.04289612 4.11215603,8.11815603 C4.03689612,8.19341595 3.99473358,8.29556674 3.995,8.402 L3.995,15.603 C3.999,15.823 4.177,16 4.401,16 L4.402,16 Z M10.402,12 L15.603,12 C15.7094333,12.0002664 15.811584,11.9581039 15.886844,11.882844 C15.9621039,11.807584 16.0042664,11.7054333 16.004,11.599 L16.004,8.398 C16.0042664,8.29156674 15.9621039,8.18941595 15.886844,8.11415603 C15.811584,8.03889612 15.7094333,7.99673358 15.603,7.997 L10.402,7.997 C10.2957402,7.99673435 10.1937416,8.03875978 10.1185106,8.11380315 C10.0432796,8.18884651 10.0009997,8.29073992 10.001,8.397 L10.001,11.6 C10.001,11.824 10.178,12 10.401,12 L10.402,12 Z M10.402,16 L15.603,16 C15.7094333,16.0002664 15.811584,15.9581039 15.886844,15.882844 C15.9621039,15.807584 16.0042664,15.7054333 16.004,15.599 L16.004,14.398 C16.0042664,14.2915667 15.9621039,14.189416 15.886844,14.114156 C15.811584,14.0388961 15.7094333,13.9967336 15.603,13.997 L10.402,13.997 C10.2957402,13.9967343 10.1937416,14.0387598 10.1185106,14.1138031 C10.0432796,14.1888465 10.0009997,14.2907399 10.001,14.397 L10.001,15.6 C10.001,15.824 10.178,16 10.401,16 L10.402,16 Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40">
|
||||
<path fill="#F0A100" fill-rule="evenodd" d="M38,0 L2.002,0 C0.896716337,-1.37884559e-07 0.000552089742,0.895716475 0,2.001 L0,38.003 C0,39.105 0.899,40.003 2,40.003 L38,40.003 C39.103,40.003 40,39.103 40,38.003 L40,2.001 C40.000531,1.47031248 39.7900198,0.961193334 39.4148607,0.585846626 C39.0397016,0.210499918 38.5306877,-0.000265742306 38,0 Z M34.003,4 C35.105,4 36.003,4.899 36.003,6 C36.003,7.102 35.103,7.998 34.003,7.998 C32.9235903,7.96385326 32.0662376,7.07894966 32.0662376,5.999 C32.0662376,4.91905034 32.9235903,4.03414674 34.003,4 Z M26.003,4 C27.105,4 28.002,4.899 28.002,6 C28.002,7.102 27.102,7.998 26.002,7.998 C24.9225903,7.96385326 24.0652376,7.07894966 24.0652376,5.999 C24.0652376,4.91905034 24.9225903,4.03414674 26.002,4 L26.003,4 Z M18.002,4 C19.104,4 20.002,4.899 20.002,6 C20.002,7.102 19.102,7.998 18.002,7.998 C16.899,7.998 16.002,7.1 16.002,5.999 C16.0025521,4.89482104 16.8978209,3.99999986 18.002,4 Z M36.002,36.002 L4,36.002 L4,12.001 L36.002,12.001 L36.002,36.002 Z M8.805,32.002 L15.196,32.002 C15.4092125,32.0030667 15.6140295,31.9189775 15.764983,31.7683995 C15.9159365,31.6178215 16.0005357,31.4132145 16,31.2 L16,16.805 C16.0005341,16.5919596 15.916072,16.3875055 15.7653354,16.2369566 C15.6145988,16.0864077 15.4100395,16.0022004 15.197,16.003 L8.794,16.003 C8.581215,16.0027342 8.37706868,16.087145 8.22660684,16.2376068 C8.07614501,16.3880687 7.99173418,16.592215 7.992,16.805 L7.992,31.208 C7.99966319,31.6507223 8.36222028,32.0048063 8.805,32.002 Z M20.803,24.002 L31.206,24.002 C31.4190404,24.0025341 31.6234945,23.918072 31.7740434,23.7673354 C31.9245923,23.6165988 32.0087996,23.4120395 32.008,23.199 L32.008,16.797 C32.0085328,16.5841335 31.9242078,16.3798319 31.7736879,16.2293121 C31.6231681,16.0787922 31.4188665,15.9944672 31.206,15.995 L20.803,15.995 C20.5901335,15.9944672 20.3858319,16.0787922 20.2353121,16.2293121 C20.0847922,16.3798319 20.0004672,16.5841335 20.001,16.797 L20.001,23.199 C20.001,23.646 20.356,24.001 20.803,24.001 L20.803,24.002 Z M20.803,32.002 L31.206,32.002 C31.4188665,32.0025328 31.6231681,31.9182078 31.7736879,31.7676879 C31.9242078,31.6171681 32.0085328,31.4128665 32.008,31.2 L32.008,28.797 C32.0085328,28.5841335 31.9242078,28.3798319 31.7736879,28.2293121 C31.6231681,28.0787922 31.4188665,27.9944672 31.206,27.995 L20.803,27.995 C20.5901335,27.9944672 20.3858319,28.0787922 20.2353121,28.2293121 C20.0847922,28.3798319 20.0004672,28.5841335 20.001,28.797 L20.001,31.2 C20.001,31.647 20.356,32.002 20.803,32.002 Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
@@ -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']);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\workspaces\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\EntityOwnerTrait;
|
||||
use Drupal\workspaces\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\workspaces\WorkspaceListBuilder",
|
||||
* "access" = "Drupal\workspaces\WorkspaceAccessControlHandler",
|
||||
* "route_provider" = {
|
||||
* "html" = "\Drupal\Core\Entity\Routing\AdminHtmlRouteProvider",
|
||||
* },
|
||||
* "form" = {
|
||||
* "default" = "\Drupal\workspaces\Form\WorkspaceForm",
|
||||
* "add" = "\Drupal\workspaces\Form\WorkspaceForm",
|
||||
* "edit" = "\Drupal\workspaces\Form\WorkspaceForm",
|
||||
* "delete" = "\Drupal\workspaces\Form\WorkspaceDeleteForm",
|
||||
* "activate" = "\Drupal\workspaces\Form\WorkspaceActivateForm",
|
||||
* "deploy" = "\Drupal\workspaces\Form\WorkspaceDeployForm",
|
||||
* },
|
||||
* },
|
||||
* admin_permission = "administer workspaces",
|
||||
* base_table = "workspace",
|
||||
* revision_table = "workspace_revision",
|
||||
* data_table = "workspace_field_data",
|
||||
* revision_data_table = "workspace_field_revision",
|
||||
* field_ui_base_route = "entity.workspace.collection",
|
||||
* entity_keys = {
|
||||
* "id" = "id",
|
||||
* "revision" = "revision_id",
|
||||
* "uuid" = "uuid",
|
||||
* "label" = "label",
|
||||
* "uid" = "uid",
|
||||
* "owner" = "uid",
|
||||
* },
|
||||
* links = {
|
||||
* "add-form" = "/admin/config/workflow/workspaces/add",
|
||||
* "edit-form" = "/admin/config/workflow/workspaces/manage/{workspace}/edit",
|
||||
* "delete-form" = "/admin/config/workflow/workspaces/manage/{workspace}/delete",
|
||||
* "activate-form" = "/admin/config/workflow/workspaces/manage/{workspace}/activate",
|
||||
* "deploy-form" = "/admin/config/workflow/workspaces/manage/{workspace}/deploy",
|
||||
* "collection" = "/admin/config/workflow/workspaces",
|
||||
* },
|
||||
* )
|
||||
*/
|
||||
class Workspace extends ContentEntityBase implements WorkspaceInterface {
|
||||
|
||||
use EntityChangedTrait;
|
||||
use EntityOwnerTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
|
||||
$fields = parent::baseFieldDefinitions($entity_type);
|
||||
$fields += static::ownerBaseFieldDefinitions($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']
|
||||
->setLabel(new TranslatableMarkup('Owner'))
|
||||
->setDescription(new TranslatableMarkup('The workspace owner.'))
|
||||
->setDisplayOptions('form', [
|
||||
'type' => 'entity_reference_autocomplete',
|
||||
'weight' => 5,
|
||||
])
|
||||
->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')
|
||||
->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 workspace was created.'));
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function publish() {
|
||||
return \Drupal::service('workspaces.operation_factory')->getPublisher($this)->publish();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isDefaultWorkspace() {
|
||||
@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;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCreatedTime() {
|
||||
return $this->get('created')->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setCreatedTime($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}
|
||||
*/
|
||||
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', []);
|
||||
$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('workspaces.manager')->purgeDeletedWorkspacesBatch();
|
||||
}
|
||||
|
||||
/**
|
||||
* Default value callback for 'uid' base field definition.
|
||||
*
|
||||
* @deprecated The ::getCurrentUserId method is deprecated in 8.6.x and will
|
||||
* be removed before 9.0.0.
|
||||
*
|
||||
* @return int[]
|
||||
* An array containing the ID of the current user.
|
||||
*/
|
||||
public static function getCurrentUserId() {
|
||||
@trigger_error('The ::getCurrentUserId method is deprecated in 8.6.x and will be removed before 9.0.0.', E_USER_DEPRECATED);
|
||||
return [\Drupal::currentUser()->id()];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\workspaces;
|
||||
|
||||
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\workspaces\WorkspaceManagerInterface
|
||||
*/
|
||||
protected $workspaceManager;
|
||||
|
||||
/**
|
||||
* Constructs a new EntityAccess instance.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager service.
|
||||
* @param \Drupal\workspaces\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('workspaces.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()) || !$this->workspaceManager->hasActiveWorkspace()) {
|
||||
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) || !$this->workspaceManager->hasActiveWorkspace()) {
|
||||
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'));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\workspaces;
|
||||
|
||||
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Entity\RevisionableInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
use Drupal\workspaces\Plugin\Validation\Constraint\EntityWorkspaceConflictConstraint;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Defines a class for reacting to entity events.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class EntityOperations implements ContainerInjectionInterface {
|
||||
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* The entity type manager service.
|
||||
*
|
||||
* @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;
|
||||
|
||||
/**
|
||||
* Constructs a new EntityOperations instance.
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
public function __construct(EntityTypeManagerInterface $entity_type_manager, WorkspaceManagerInterface $workspace_manager, WorkspaceAssociationInterface $workspace_association) {
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->workspaceManager = $workspace_manager;
|
||||
$this->workspaceAssociation = $workspace_association;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('entity_type.manager'),
|
||||
$container->get('workspaces.manager'),
|
||||
$container->get('workspaces.association')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Acts on entity IDs before they are loaded.
|
||||
*
|
||||
* @see hook_entity_preload()
|
||||
*/
|
||||
public function entityPreload(array $ids, $entity_type_id) {
|
||||
$entities = [];
|
||||
|
||||
// Only run if the entity type can belong to a workspace and we are in a
|
||||
// non-default workspace.
|
||||
if (!$this->workspaceManager->shouldAlterOperations($this->entityTypeManager->getDefinition($entity_type_id))) {
|
||||
return $entities;
|
||||
}
|
||||
|
||||
// Get a list of revision IDs for entities that have a revision set for the
|
||||
// current active workspace. If an entity has multiple revisions set for a
|
||||
// workspace, only the one with the highest ID is returned.
|
||||
if ($tracked_entities = $this->workspaceAssociation->getTrackedEntities($this->workspaceManager->getActiveWorkspace()->id(), $entity_type_id, $ids)) {
|
||||
/** @var \Drupal\Core\Entity\RevisionableStorageInterface $storage */
|
||||
$storage = $this->entityTypeManager->getStorage($entity_type_id);
|
||||
|
||||
// Swap out every entity which has a revision set for the current active
|
||||
// workspace.
|
||||
foreach ($storage->loadMultipleRevisions(array_keys($tracked_entities[$entity_type_id])) as $revision) {
|
||||
$entities[$revision->id()] = $revision;
|
||||
}
|
||||
}
|
||||
|
||||
return $entities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Acts on an entity before it is created or updated.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity being saved.
|
||||
*
|
||||
* @see hook_entity_presave()
|
||||
*/
|
||||
public function entityPresave(EntityInterface $entity) {
|
||||
$entity_type = $entity->getEntityType();
|
||||
|
||||
// Only run if we are not dealing with an entity type provided by the
|
||||
// Workspaces module, an internal entity type or if we are in a non-default
|
||||
// workspace.
|
||||
if ($this->shouldSkipPreOperations($entity_type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Disallow any change to an unsupported entity when we are not in the
|
||||
// default workspace.
|
||||
if (!$this->workspaceManager->isEntityTypeSupported($entity_type)) {
|
||||
throw new \RuntimeException('This entity can only be saved in the default workspace.');
|
||||
}
|
||||
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface|\Drupal\Core\Entity\EntityPublishedInterface $entity */
|
||||
if (!$entity->isNew() && !$entity->isSyncing()) {
|
||||
// Force a new revision if the entity is not replicating.
|
||||
$entity->setNewRevision(TRUE);
|
||||
|
||||
// All entities in the non-default workspace are pending revisions,
|
||||
// regardless of their publishing status. This means that when creating
|
||||
// a published pending revision in a non-default workspace it will also be
|
||||
// a published pending revision in the default workspace, however, it will
|
||||
// become the default revision only when it is replicated to the default
|
||||
// workspace.
|
||||
$entity->isDefaultRevision(FALSE);
|
||||
|
||||
// Track the workspaces in which the new revision was saved.
|
||||
$field_name = $entity_type->getRevisionMetadataKey('workspace');
|
||||
$entity->{$field_name}->target_id = $this->workspaceManager->getActiveWorkspace()->id();
|
||||
}
|
||||
|
||||
// When a new published entity is inserted in a non-default workspace, we
|
||||
// actually want two revisions to be saved:
|
||||
// - An unpublished default revision in the default ('live') workspace.
|
||||
// - A published pending revision in the current workspace.
|
||||
if ($entity->isNew() && $entity->isPublished()) {
|
||||
// Keep track of the publishing status in a dynamic property for
|
||||
// ::entityInsert(), then unpublish the default revision.
|
||||
// @todo Remove this dynamic property once we have an API for associating
|
||||
// temporary data with an entity: https://www.drupal.org/node/2896474.
|
||||
$entity->_initialPublished = TRUE;
|
||||
$entity->setUnpublished();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Responds to the creation of a new entity.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity that was just saved.
|
||||
*
|
||||
* @see hook_entity_insert()
|
||||
*/
|
||||
public function entityInsert(EntityInterface $entity) {
|
||||
/** @var \Drupal\Core\Entity\RevisionableInterface|\Drupal\Core\Entity\EntityPublishedInterface $entity */
|
||||
// Only run if the entity type can belong to a workspace and we are in a
|
||||
// non-default workspace.
|
||||
if (!$this->workspaceManager->shouldAlterOperations($entity->getEntityType())) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->workspaceAssociation->trackEntity($entity, $this->workspaceManager->getActiveWorkspace());
|
||||
|
||||
// When an entity is newly created in a workspace, it should be published in
|
||||
// that workspace, but not yet published on the live workspace. It is first
|
||||
// saved as unpublished for the default revision, then immediately a second
|
||||
// revision is created which is published and attached to the workspace.
|
||||
// This ensures that the published version of the entity does not 'leak'
|
||||
// into the live site. This differs from edits to existing entities where
|
||||
// there is already a valid default revision for the live workspace.
|
||||
if (isset($entity->_initialPublished)) {
|
||||
// Operate on a clone to avoid changing the entity prior to subsequent
|
||||
// hook_entity_insert() implementations.
|
||||
$pending_revision = clone $entity;
|
||||
$pending_revision->setPublished();
|
||||
$pending_revision->isDefaultRevision(FALSE);
|
||||
$pending_revision->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Responds to updates to an entity.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity that was just saved.
|
||||
*
|
||||
* @see hook_entity_update()
|
||||
*/
|
||||
public function entityUpdate(EntityInterface $entity) {
|
||||
// Only run if the entity type can belong to a workspace and we are in a
|
||||
// non-default workspace.
|
||||
if (!$this->workspaceManager->shouldAlterOperations($entity->getEntityType())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only track new revisions.
|
||||
/** @var \Drupal\Core\Entity\RevisionableInterface $entity */
|
||||
if ($entity->getLoadedRevisionId() != $entity->getRevisionId()) {
|
||||
$this->workspaceAssociation->trackEntity($entity, $this->workspaceManager->getActiveWorkspace());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Acts on an entity before it is deleted.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity being deleted.
|
||||
*
|
||||
* @see hook_entity_predelete()
|
||||
*/
|
||||
public function entityPredelete(EntityInterface $entity) {
|
||||
$entity_type = $entity->getEntityType();
|
||||
|
||||
// Only run if we are not dealing with an entity type provided by the
|
||||
// Workspaces module, an internal entity type or if we are in a non-default
|
||||
// workspace.
|
||||
if ($this->shouldSkipPreOperations($entity_type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Disallow any change to an unsupported entity when we are not in the
|
||||
// default workspace.
|
||||
if (!$this->workspaceManager->isEntityTypeSupported($entity_type)) {
|
||||
throw new \RuntimeException('This entity can only be deleted in the default workspace.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Alters entity forms to disallow concurrent editing in multiple workspaces.
|
||||
*
|
||||
* @param array $form
|
||||
* An associative array containing the structure of the form.
|
||||
* @param \Drupal\Core\Form\FormStateInterface $form_state
|
||||
* The current state of the form.
|
||||
* @param string $form_id
|
||||
* The form ID.
|
||||
*
|
||||
* @see hook_form_alter()
|
||||
*/
|
||||
public function entityFormAlter(array &$form, FormStateInterface $form_state, $form_id) {
|
||||
/** @var \Drupal\Core\Entity\RevisionableInterface $entity */
|
||||
$entity = $form_state->getFormObject()->getEntity();
|
||||
if (!$this->workspaceManager->isEntityTypeSupported($entity->getEntityType())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// For supported entity types, signal the fact that this form is safe to use
|
||||
// in a non-default workspace.
|
||||
// @see \Drupal\workspaces\FormOperations::validateForm()
|
||||
$form_state->set('workspace_safe', TRUE);
|
||||
|
||||
// Add an entity builder to the form which marks the edited entity object as
|
||||
// a pending revision. This is needed so validation constraints like
|
||||
// \Drupal\path\Plugin\Validation\Constraint\PathAliasConstraintValidator
|
||||
// know in advance (before hook_entity_presave()) that the new revision will
|
||||
// be a pending one.
|
||||
if ($this->workspaceManager->hasActiveWorkspace()) {
|
||||
$form['#entity_builders'][] = [get_called_class(), 'entityFormEntityBuild'];
|
||||
}
|
||||
|
||||
// Run the workspace conflict validation constraint when the entity form is
|
||||
// being built so we can "disable" it early and display a message to the
|
||||
// user, instead of allowing them to enter data that can never be saved.
|
||||
foreach ($entity->validate()->getEntityViolations() as $violation) {
|
||||
if ($violation->getConstraint() instanceof EntityWorkspaceConflictConstraint) {
|
||||
$form['#markup'] = $violation->getMessage();
|
||||
$form['#access'] = FALSE;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Entity builder that marks all supported entities as pending revisions.
|
||||
*/
|
||||
public static function entityFormEntityBuild($entity_type_id, RevisionableInterface $entity, &$form, FormStateInterface &$form_state) {
|
||||
// Set the non-default revision flag so that validation constraints are also
|
||||
// aware that a pending revision is about to be created.
|
||||
$entity->isDefaultRevision(FALSE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether we need to react on pre-save or pre-delete operations.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
|
||||
* The entity type to check.
|
||||
*
|
||||
* @return bool
|
||||
* Returns TRUE if the pre-save or pre-delete entity operations should not
|
||||
* be altered in the current request, FALSE otherwise.
|
||||
*/
|
||||
protected function shouldSkipPreOperations(EntityTypeInterface $entity_type) {
|
||||
// We should not react on pre-save and pre-delete entity operations if one
|
||||
// of the following conditions are met:
|
||||
// - the entity type is provided by the Workspaces module;
|
||||
// - 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->hasActiveWorkspace();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\workspaces\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\workspaces\WorkspaceManagerInterface;
|
||||
|
||||
/**
|
||||
* Workspaces PostgreSQL-specific entity query implementation.
|
||||
*/
|
||||
class PgsqlQueryFactory extends BaseQueryFactory {
|
||||
|
||||
/**
|
||||
* The workspace manager.
|
||||
*
|
||||
* @var \Drupal\workspaces\WorkspaceManagerInterface
|
||||
*/
|
||||
protected $workspaceManager;
|
||||
|
||||
/**
|
||||
* Constructs a PgsqlQueryFactory object.
|
||||
*
|
||||
* @param \Drupal\Core\Database\Connection $connection
|
||||
* The database connection used by the entity query.
|
||||
* @param \Drupal\workspaces\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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\workspaces\EntityQuery;
|
||||
|
||||
use Drupal\Core\Entity\Query\Sql\Query as BaseQuery;
|
||||
|
||||
/**
|
||||
* Alters entity queries to use a workspace revision instead of the default one.
|
||||
*/
|
||||
class Query extends BaseQuery {
|
||||
|
||||
use QueryTrait {
|
||||
prepare as traitPrepare;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function prepare() {
|
||||
$this->traitPrepare();
|
||||
|
||||
// If the prepare() method from the trait decided that we need to alter this
|
||||
// query, we need to re-define the key fields for fetchAllKeyed() as SQL
|
||||
// expressions.
|
||||
if ($this->sqlQuery->getMetaData('active_workspace_id')) {
|
||||
$id_field = $this->entityType->getKey('id');
|
||||
$revision_field = $this->entityType->getKey('revision');
|
||||
|
||||
// Since the query is against the base table, we have to take into account
|
||||
// that the revision ID might come from the workspace_association
|
||||
// relationship, and, as a consequence, the revision ID field is no longer
|
||||
// a simple SQL field but an expression.
|
||||
$this->sqlFields = [];
|
||||
$this->sqlQuery->addExpression("COALESCE(workspace_association.target_entity_revision_id, base_table.$revision_field)", $revision_field);
|
||||
$this->sqlQuery->addExpression("base_table.$id_field", $id_field);
|
||||
|
||||
$this->sqlGroupBy['workspace_association.target_entity_revision_id'] = 'workspace_association.target_entity_revision_id';
|
||||
$this->sqlGroupBy["base_table.$id_field"] = "base_table.$id_field";
|
||||
$this->sqlGroupBy["base_table.$revision_field"] = "base_table.$revision_field";
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\workspaces\EntityQuery;
|
||||
|
||||
use Drupal\Core\Entity\Query\Sql\QueryAggregate as BaseQueryAggregate;
|
||||
|
||||
/**
|
||||
* Alters aggregate entity queries to use a workspace revision if possible.
|
||||
*/
|
||||
class QueryAggregate extends BaseQueryAggregate {
|
||||
|
||||
use QueryTrait {
|
||||
prepare as traitPrepare;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function prepare() {
|
||||
// Aggregate entity queries do not return an array of entity IDs keyed by
|
||||
// revision IDs, they only return the values of the aggregated fields, so we
|
||||
// don't need to add any expressions like we do in
|
||||
// \Drupal\workspaces\EntityQuery\Query::prepare().
|
||||
$this->traitPrepare();
|
||||
|
||||
// Throw away the ID fields.
|
||||
$this->sqlFields = [];
|
||||
return $this;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\workspaces\EntityQuery;
|
||||
|
||||
use Drupal\Core\Database\Connection;
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Entity\Query\QueryBase;
|
||||
use Drupal\Core\Entity\Query\Sql\QueryFactory as BaseQueryFactory;
|
||||
use Drupal\workspaces\WorkspaceManagerInterface;
|
||||
|
||||
/**
|
||||
* Workspaces-specific entity query implementation.
|
||||
*/
|
||||
class QueryFactory extends BaseQueryFactory {
|
||||
|
||||
/**
|
||||
* The workspace manager.
|
||||
*
|
||||
* @var \Drupal\workspaces\WorkspaceManagerInterface
|
||||
*/
|
||||
protected $workspaceManager;
|
||||
|
||||
/**
|
||||
* Constructs a QueryFactory object.
|
||||
*
|
||||
* @param \Drupal\Core\Database\Connection $connection
|
||||
* The database connection used by the entity query.
|
||||
* @param \Drupal\workspaces\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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\workspaces\EntityQuery;
|
||||
|
||||
use Drupal\Core\Database\Connection;
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\workspaces\WorkspaceManagerInterface;
|
||||
|
||||
/**
|
||||
* Provides workspaces-specific helpers for altering entity queries.
|
||||
*/
|
||||
trait QueryTrait {
|
||||
|
||||
/**
|
||||
* The workspace manager.
|
||||
*
|
||||
* @var \Drupal\workspaces\WorkspaceManagerInterface
|
||||
*/
|
||||
protected $workspaceManager;
|
||||
|
||||
/**
|
||||
* Constructs a Query object.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
|
||||
* The entity type definition.
|
||||
* @param string $conjunction
|
||||
* - AND: all of the conditions on the query need to match.
|
||||
* - OR: at least one of the conditions on the query need to match.
|
||||
* @param \Drupal\Core\Database\Connection $connection
|
||||
* The database connection to run the query against.
|
||||
* @param array $namespaces
|
||||
* List of potential namespaces of the classes belonging to this query.
|
||||
* @param \Drupal\workspaces\WorkspaceManagerInterface $workspace_manager
|
||||
* The workspace manager.
|
||||
*/
|
||||
public function __construct(EntityTypeInterface $entity_type, $conjunction, Connection $connection, array $namespaces, WorkspaceManagerInterface $workspace_manager) {
|
||||
parent::__construct($entity_type, $conjunction, $connection, $namespaces);
|
||||
|
||||
$this->workspaceManager = $workspace_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function prepare() {
|
||||
parent::prepare();
|
||||
|
||||
// Do not alter entity revision queries.
|
||||
// @todo How about queries for the latest revision? Should we alter them to
|
||||
// look for the latest workspace-specific revision?
|
||||
if ($this->allRevisions) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
// Only alter the query if the active workspace is not the default one and
|
||||
// the entity type is supported.
|
||||
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);
|
||||
|
||||
// LEFT JOIN 'workspace_association' to the base table of the query so we
|
||||
// can properly include live content along with a possible workspace
|
||||
// revision.
|
||||
$id_field = $this->entityType->getKey('id');
|
||||
$this->sqlQuery->leftJoin('workspace_association', 'workspace_association', "%alias.target_entity_type_id = '{$this->entityTypeId}' AND %alias.target_entity_id = base_table.$id_field AND %alias.workspace = '{$active_workspace->id()}'");
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isSimpleQuery() {
|
||||
// We declare that this is not a simple query in
|
||||
// \Drupal\workspaces\EntityQuery\QueryTrait::prepare(), but that's not
|
||||
// enough because the parent method can return TRUE in some circumstances.
|
||||
if ($this->sqlQuery->getMetaData('active_workspace_id')) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
return parent::isSimpleQuery();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\workspaces\EntityQuery;
|
||||
|
||||
use Drupal\Core\Database\Query\SelectInterface;
|
||||
use Drupal\Core\Entity\EntityType;
|
||||
use Drupal\Core\Entity\Query\Sql\Tables as BaseTables;
|
||||
use Drupal\Core\Field\FieldStorageDefinitionInterface;
|
||||
|
||||
/**
|
||||
* Alters entity queries to use a workspace revision instead of the default one.
|
||||
*/
|
||||
class Tables extends BaseTables {
|
||||
|
||||
/**
|
||||
* The workspace manager.
|
||||
*
|
||||
* @var \Drupal\workspaces\WorkspaceManagerInterface
|
||||
*/
|
||||
protected $workspaceManager;
|
||||
|
||||
/**
|
||||
* Workspace association table array, key is base table name, value is alias.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $contentWorkspaceTables = [];
|
||||
|
||||
/**
|
||||
* Keeps track of the entity type IDs for each base table of the query.
|
||||
*
|
||||
* The array is keyed by the base table alias and the values are entity type
|
||||
* IDs.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $baseTablesEntityType = [];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __construct(SelectInterface $sql_query) {
|
||||
parent::__construct($sql_query);
|
||||
|
||||
$this->workspaceManager = \Drupal::service('workspaces.manager');
|
||||
|
||||
// The join between the first 'workspace_association' table and base table
|
||||
// of the query is done in
|
||||
// \Drupal\workspaces\EntityQuery\QueryTrait::prepare(), so we need to
|
||||
// initialize its entry manually.
|
||||
if ($this->sqlQuery->getMetaData('active_workspace_id')) {
|
||||
$this->contentWorkspaceTables['base_table'] = 'workspace_association';
|
||||
$this->baseTablesEntityType['base_table'] = $this->sqlQuery->getMetaData('entity_type');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function addField($field, $type, $langcode) {
|
||||
// The parent method uses shared and dedicated revision tables only when the
|
||||
// entity query is instructed to query all revisions. However, if we are
|
||||
// looking for workspace-specific revisions, we have to force the parent
|
||||
// method to always pick the revision tables if the field being queried is
|
||||
// revisionable.
|
||||
if ($active_workspace_id = $this->sqlQuery->getMetaData('active_workspace_id')) {
|
||||
$previous_all_revisions = $this->sqlQuery->getMetaData('all_revisions');
|
||||
$this->sqlQuery->addMetaData('all_revisions', TRUE);
|
||||
}
|
||||
|
||||
$alias = parent::addField($field, $type, $langcode);
|
||||
|
||||
// Restore the 'all_revisions' metadata because we don't want to interfere
|
||||
// with the rest of the query.
|
||||
if (isset($previous_all_revisions)) {
|
||||
$this->sqlQuery->addMetaData('all_revisions', $previous_all_revisions);
|
||||
}
|
||||
|
||||
return $alias;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function addJoin($type, $table, $join_condition, $langcode, $delta = NULL) {
|
||||
if ($this->sqlQuery->getMetaData('active_workspace_id')) {
|
||||
// The join condition for a shared or dedicated field table is in the form
|
||||
// of "%alias.$id_field = $base_table.$id_field". Whenever we join a field
|
||||
// table we have to check:
|
||||
// 1) if $base_table is of an entity type that can belong to a workspace;
|
||||
// 2) if $id_field is the revision key of that entity type or the special
|
||||
// 'revision_id' string used when joining dedicated field tables.
|
||||
// If those two conditions are met, we have to update the join condition
|
||||
// to also look for a possible workspace-specific revision using COALESCE.
|
||||
$condition_parts = explode(' = ', $join_condition);
|
||||
list($base_table, $id_field) = explode('.', $condition_parts[1]);
|
||||
|
||||
if (isset($this->baseTablesEntityType[$base_table])) {
|
||||
$entity_type_id = $this->baseTablesEntityType[$base_table];
|
||||
$revision_key = $this->entityTypeManager->getActiveDefinition($entity_type_id)->getKey('revision');
|
||||
|
||||
if ($id_field === $revision_key || $id_field === 'revision_id') {
|
||||
$workspace_association_table = $this->contentWorkspaceTables[$base_table];
|
||||
$join_condition = "{$condition_parts[0]} = COALESCE($workspace_association_table.target_entity_revision_id, {$condition_parts[1]})";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parent::addJoin($type, $table, $join_condition, $langcode, $delta);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function addNextBaseTable(EntityType $entity_type, $table, $sql_column, FieldStorageDefinitionInterface $field_storage) {
|
||||
$next_base_table_alias = parent::addNextBaseTable($entity_type, $table, $sql_column, $field_storage);
|
||||
|
||||
$active_workspace_id = $this->sqlQuery->getMetaData('active_workspace_id');
|
||||
if ($active_workspace_id && $this->workspaceManager->isEntityTypeSupported($entity_type)) {
|
||||
$this->addWorkspaceAssociationJoin($entity_type->id(), $next_base_table_alias, $active_workspace_id);
|
||||
}
|
||||
|
||||
return $next_base_table_alias;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new join to the 'workspace_association' table for an entity base table.
|
||||
*
|
||||
* This method assumes that the active workspace has already been determined
|
||||
* to be a non-default workspace.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The ID of the entity type whose base table we are joining.
|
||||
* @param string $base_table_alias
|
||||
* The alias of the entity type's base table.
|
||||
* @param string $active_workspace_id
|
||||
* The ID of the active workspace.
|
||||
*
|
||||
* @return string
|
||||
* The alias of the joined table.
|
||||
*/
|
||||
public function addWorkspaceAssociationJoin($entity_type_id, $base_table_alias, $active_workspace_id) {
|
||||
if (!isset($this->contentWorkspaceTables[$base_table_alias])) {
|
||||
$entity_type = $this->entityTypeManager->getActiveDefinition($entity_type_id);
|
||||
$id_field = $entity_type->getKey('id');
|
||||
|
||||
// LEFT join the Workspace association entity's table so we can properly
|
||||
// include live content along with a possible workspace-specific revision.
|
||||
$this->contentWorkspaceTables[$base_table_alias] = $this->sqlQuery->leftJoin('workspace_association', NULL, "%alias.target_entity_type_id = '$entity_type_id' AND %alias.target_entity_id = $base_table_alias.$id_field AND %alias.workspace = '$active_workspace_id'");
|
||||
|
||||
$this->baseTablesEntityType[$base_table_alias] = $entity_type->id();
|
||||
}
|
||||
return $this->contentWorkspaceTables[$base_table_alias];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\workspaces;
|
||||
|
||||
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Field\BaseFieldDefinition;
|
||||
use Drupal\Core\StringTranslation\TranslatableMarkup;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Manipulates entity type information.
|
||||
*
|
||||
* This class contains primarily bridged hooks for compile-time or
|
||||
* cache-clear-time hooks. Runtime hooks should be placed in EntityOperations.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class EntityTypeInfo implements ContainerInjectionInterface {
|
||||
|
||||
/**
|
||||
* The entity type manager service.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The workspace manager service.
|
||||
*
|
||||
* @var \Drupal\workspaces\WorkspaceManagerInterface
|
||||
*/
|
||||
protected $workspaceManager;
|
||||
|
||||
/**
|
||||
* Constructs a new EntityTypeInfo instance.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager service.
|
||||
* @param \Drupal\workspaces\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('workspaces.manager')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the "EntityWorkspaceConflict" constraint to eligible entity types.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeInterface[] $entity_types
|
||||
* An associative array of all entity type definitions, keyed by the entity
|
||||
* type name. Passed by reference.
|
||||
*
|
||||
* @see hook_entity_type_build()
|
||||
*/
|
||||
public function entityTypeBuild(array &$entity_types) {
|
||||
foreach ($entity_types as $entity_type) {
|
||||
if ($this->workspaceManager->isEntityTypeSupported($entity_type)) {
|
||||
$entity_type->addConstraint('EntityWorkspaceConflict');
|
||||
$entity_type->setRevisionMetadataKey('workspace', 'workspace');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the 'latest-version' link template provided by Content Moderation.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeInterface[] $entity_types
|
||||
* An array of entity types.
|
||||
*
|
||||
* @see hook_entity_type_alter()
|
||||
*/
|
||||
public function entityTypeAlter(array &$entity_types) {
|
||||
foreach ($entity_types as $entity_type_id => $entity_type) {
|
||||
// Non-default workspaces display the active revision on the canonical
|
||||
// route of an entity, so the latest version route is no longer needed.
|
||||
$link_templates = $entity_type->get('links');
|
||||
unset($link_templates['latest-version']);
|
||||
$entity_type->set('links', $link_templates);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Alters field plugin definitions.
|
||||
*
|
||||
* @param array[] $definitions
|
||||
* An array of field plugin definitions.
|
||||
*
|
||||
* @see hook_field_info_alter()
|
||||
*/
|
||||
public function fieldInfoAlter(&$definitions) {
|
||||
if (isset($definitions['entity_reference'])) {
|
||||
$definitions['entity_reference']['constraints']['EntityReferenceSupportedNewEntities'] = [];
|
||||
}
|
||||
|
||||
// Allow path aliases to be changed in workspace-specific pending revisions.
|
||||
if (isset($definitions['path'])) {
|
||||
unset($definitions['path']['constraints']['PathAlias']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides custom base field definitions for a content entity type.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
|
||||
* The entity type definition.
|
||||
*
|
||||
* @return \Drupal\Core\Field\FieldDefinitionInterface[]
|
||||
* An array of field definitions, keyed by field name.
|
||||
*
|
||||
* @see hook_entity_base_field_info()
|
||||
*/
|
||||
public function entityBaseFieldInfo(EntityTypeInterface $entity_type) {
|
||||
if ($this->workspaceManager->isEntityTypeSupported($entity_type)) {
|
||||
// Disable the BC layer to prevent a recursion, this only needs the
|
||||
// workspace key that is always set.
|
||||
$field_name = $entity_type->getRevisionMetadataKeys(FALSE)['workspace'];
|
||||
$fields[$field_name] = BaseFieldDefinition::create('entity_reference')
|
||||
->setLabel(new TranslatableMarkup('Workspace'))
|
||||
->setDescription(new TranslatableMarkup('Indicates the workspace that this revision belongs to.'))
|
||||
->setSetting('target_type', 'workspace')
|
||||
->setInternal(TRUE)
|
||||
->setTranslatable(FALSE)
|
||||
->setRevisionable(TRUE);
|
||||
|
||||
return $fields;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\workspaces\EventSubscriber;
|
||||
|
||||
use Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface;
|
||||
use Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface;
|
||||
use Drupal\Core\Entity\EntityTypeEventSubscriberTrait;
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Entity\EntityTypeListenerInterface;
|
||||
use Drupal\Core\Field\BaseFieldDefinition;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
use Drupal\workspaces\WorkspaceManagerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
/**
|
||||
* Defines a class for listening to entity schema changes.
|
||||
*/
|
||||
class EntitySchemaSubscriber implements EntityTypeListenerInterface, EventSubscriberInterface {
|
||||
|
||||
use EntityTypeEventSubscriberTrait;
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* The definition update manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface
|
||||
*/
|
||||
protected $entityDefinitionUpdateManager;
|
||||
|
||||
/**
|
||||
* The last installed schema definitions.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface
|
||||
*/
|
||||
protected $entityLastInstalledSchemaRepository;
|
||||
|
||||
/**
|
||||
* The workspace manager.
|
||||
*
|
||||
* @var \Drupal\workspaces\WorkspaceManagerInterface
|
||||
*/
|
||||
protected $workspaceManager;
|
||||
|
||||
/**
|
||||
* Constructs a new EntitySchemaSubscriber.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface $entityDefinitionUpdateManager
|
||||
* Definition update manager.
|
||||
* @param \Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface $entityLastInstalledSchemaRepository
|
||||
* Last definitions.
|
||||
* @param \Drupal\workspaces\WorkspaceManagerInterface $workspace_manager
|
||||
* The workspace manager.
|
||||
*/
|
||||
public function __construct(EntityDefinitionUpdateManagerInterface $entityDefinitionUpdateManager, EntityLastInstalledSchemaRepositoryInterface $entityLastInstalledSchemaRepository, WorkspaceManagerInterface $workspace_manager) {
|
||||
$this->entityDefinitionUpdateManager = $entityDefinitionUpdateManager;
|
||||
$this->entityLastInstalledSchemaRepository = $entityLastInstalledSchemaRepository;
|
||||
$this->workspaceManager = $workspace_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function getSubscribedEvents() {
|
||||
return static::getEntityTypeEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function onEntityTypeCreate(EntityTypeInterface $entity_type) {
|
||||
// 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
|
||||
// If the entity type is now supported by Workspaces, add the revision
|
||||
// metadata field.
|
||||
if ($this->workspaceManager->isEntityTypeSupported($entity_type) && !$this->workspaceManager->isEntityTypeSupported($original)) {
|
||||
$this->addRevisionMetadataField($entity_type);
|
||||
}
|
||||
|
||||
// If the entity type is no longer supported by Workspaces, remove the
|
||||
// revision metadata field.
|
||||
if ($this->workspaceManager->isEntityTypeSupported($original) && !$this->workspaceManager->isEntityTypeSupported($entity_type)) {
|
||||
$revision_metadata_keys = $original->get('revision_metadata_keys');
|
||||
$field_storage_definition = $this->entityLastInstalledSchemaRepository->getLastInstalledFieldStorageDefinitions($entity_type->id())[$revision_metadata_keys['workspace']];
|
||||
$this->entityDefinitionUpdateManager->uninstallFieldStorageDefinition($field_storage_definition);
|
||||
|
||||
// We are only removing a revision metadata key so we don't need to go
|
||||
// through the entity update process.
|
||||
$entity_type->setRevisionMetadataKey('workspace', NULL);
|
||||
$this->entityLastInstalledSchemaRepository->setLastInstalledDefinition($entity_type);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function onFieldableEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original, array $field_storage_definitions, array $original_field_storage_definitions, array &$sandbox = NULL) {
|
||||
$this->onEntityTypeUpdate($entity_type, $original);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function onEntityTypeDelete(EntityTypeInterface $entity_type) {
|
||||
// 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) {
|
||||
if (!$entity_type->hasRevisionMetadataKey('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.");
|
||||
}
|
||||
|
||||
// We are only adding a revision metadata key so we don't need to go
|
||||
// through the entity update process.
|
||||
$entity_type->setRevisionMetadataKey('workspace', 'workspace');
|
||||
$this->entityLastInstalledSchemaRepository->setLastInstalledDefinition($entity_type);
|
||||
}
|
||||
|
||||
$this->entityDefinitionUpdateManager->installFieldStorageDefinition($entity_type->getRevisionMetadataKey('workspace'), $entity_type->id(), 'workspaces', $this->getWorkspaceFieldDefinition());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the base field definition for the 'workspace' revision metadata field.
|
||||
*
|
||||
* @return \Drupal\Core\Field\BaseFieldDefinition
|
||||
* The base field definition.
|
||||
*/
|
||||
protected function getWorkspaceFieldDefinition() {
|
||||
return BaseFieldDefinition::create('entity_reference')
|
||||
->setLabel($this->t('Workspace'))
|
||||
->setDescription($this->t('Indicates the workspace that this revision belongs to.'))
|
||||
->setSetting('target_type', 'workspace')
|
||||
->setInternal(TRUE)
|
||||
->setTranslatable(FALSE)
|
||||
->setRevisionable(TRUE);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\workspaces\EventSubscriber;
|
||||
|
||||
use Drupal\path_alias\AliasManagerInterface;
|
||||
use Drupal\Core\Path\CurrentPathStack;
|
||||
use Drupal\Core\Routing\CacheableRouteProviderInterface;
|
||||
use Drupal\Core\Routing\RouteProviderInterface;
|
||||
use Drupal\workspaces\WorkspaceManagerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
|
||||
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
|
||||
/**
|
||||
* Provides a event subscriber for setting workspace-specific cache keys.
|
||||
*/
|
||||
class WorkspaceRequestSubscriber implements EventSubscriberInterface {
|
||||
|
||||
/**
|
||||
* The alias manager that caches alias lookups based on the request.
|
||||
*
|
||||
* @var \Drupal\path_alias\AliasManagerInterface
|
||||
*/
|
||||
protected $aliasManager;
|
||||
|
||||
/**
|
||||
* The current path.
|
||||
*
|
||||
* @var \Drupal\Core\Path\CurrentPathStack
|
||||
*/
|
||||
protected $currentPath;
|
||||
|
||||
/**
|
||||
* The route provider to load routes by name.
|
||||
*
|
||||
* @var \Drupal\Core\Routing\RouteProviderInterface
|
||||
*/
|
||||
protected $routeProvider;
|
||||
|
||||
/**
|
||||
* The workspace manager.
|
||||
*
|
||||
* @var \Drupal\workspaces\WorkspaceManagerInterface
|
||||
*/
|
||||
protected $workspaceManager;
|
||||
|
||||
/**
|
||||
* Constructs a new WorkspaceRequestSubscriber instance.
|
||||
*
|
||||
* @param \Drupal\path_alias\AliasManagerInterface $alias_manager
|
||||
* The alias manager.
|
||||
* @param \Drupal\Core\Path\CurrentPathStack $current_path
|
||||
* The current path.
|
||||
* @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
|
||||
* The route provider.
|
||||
* @param \Drupal\workspaces\WorkspaceManagerInterface $workspace_manager
|
||||
* The workspace manager.
|
||||
*/
|
||||
public function __construct(AliasManagerInterface $alias_manager, CurrentPathStack $current_path, RouteProviderInterface $route_provider, WorkspaceManagerInterface $workspace_manager) {
|
||||
$this->aliasManager = $alias_manager;
|
||||
$this->currentPath = $current_path;
|
||||
$this->routeProvider = $route_provider;
|
||||
$this->workspaceManager = $workspace_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the cache key on the alias manager cache decorator.
|
||||
*
|
||||
* KernelEvents::CONTROLLER is used in order to be executed after routing.
|
||||
*
|
||||
* @param \Symfony\Component\HttpKernel\Event\FilterControllerEvent $event
|
||||
* The Event to process.
|
||||
*/
|
||||
public function onKernelController(FilterControllerEvent $event) {
|
||||
// Set the cache key on the alias manager cache decorator.
|
||||
if ($event->isMasterRequest() && $this->workspaceManager->hasActiveWorkspace()) {
|
||||
$cache_key = $this->workspaceManager->getActiveWorkspace()->id() . ':' . rtrim($this->currentPath->getPath($event->getRequest()), '/');
|
||||
$this->aliasManager->setCacheKey($cache_key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the active workspace as a cache key part to the route provider.
|
||||
*
|
||||
* @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
|
||||
* An event object.
|
||||
*/
|
||||
public function onKernelRequest(GetResponseEvent $event) {
|
||||
if ($this->workspaceManager->hasActiveWorkspace() && $this->routeProvider instanceof CacheableRouteProviderInterface) {
|
||||
$this->routeProvider->addExtraCacheKeyPart('workspace', $this->workspaceManager->getActiveWorkspace()->id());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public static function getSubscribedEvents() {
|
||||
// Use a priority of 190 in order to run after the generic core subscriber.
|
||||
// @see \Drupal\Core\EventSubscriber\PathSubscriber::getSubscribedEvents()
|
||||
$events[KernelEvents::CONTROLLER][] = ['onKernelController', 190];
|
||||
|
||||
// Use a priority of 33 in order to run before Symfony's router listener.
|
||||
// @see \Symfony\Component\HttpKernel\EventListener\RouterListener::getSubscribedEvents()
|
||||
$events[KernelEvents::REQUEST][] = ['onKernelRequest', 33];
|
||||
|
||||
return $events;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.'));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\workspaces\Form;
|
||||
|
||||
use Drupal\Core\Entity\EntityConfirmFormBase;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Messenger\MessengerInterface;
|
||||
use Drupal\workspaces\WorkspaceAccessException;
|
||||
use Drupal\workspaces\WorkspaceManagerInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Handle activation of a workspace on administrative pages.
|
||||
*/
|
||||
class WorkspaceActivateForm extends EntityConfirmFormBase implements WorkspaceFormInterface {
|
||||
|
||||
/**
|
||||
* The workspace entity.
|
||||
*
|
||||
* @var \Drupal\workspaces\WorkspaceInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* The workspace replication manager.
|
||||
*
|
||||
* @var \Drupal\workspaces\WorkspaceManagerInterface
|
||||
*/
|
||||
protected $workspaceManager;
|
||||
|
||||
/**
|
||||
* The messenger service.
|
||||
*
|
||||
* @var \Drupal\Core\Messenger\MessengerInterface
|
||||
*/
|
||||
protected $messenger;
|
||||
|
||||
/**
|
||||
* Constructs a new WorkspaceActivateForm.
|
||||
*
|
||||
* @param \Drupal\workspaces\WorkspaceManagerInterface $workspace_manager
|
||||
* The workspace manager.
|
||||
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
|
||||
* The messenger service.
|
||||
*/
|
||||
public function __construct(WorkspaceManagerInterface $workspace_manager, MessengerInterface $messenger) {
|
||||
$this->workspaceManager = $workspace_manager;
|
||||
$this->messenger = $messenger;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('workspaces.manager'),
|
||||
$container->get('messenger')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getQuestion() {
|
||||
return $this->t('Would you like to activate the %workspace workspace?', ['%workspace' => $this->entity->label()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getDescription() {
|
||||
return $this->t('Activate the %workspace workspace.', ['%workspace' => $this->entity->label()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCancelUrl() {
|
||||
return $this->entity->toUrl('collection');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(array $form, FormStateInterface $form_state) {
|
||||
$form = parent::buildForm($form, $form_state);
|
||||
|
||||
// Content entity forms do not use the parent's #after_build callback.
|
||||
unset($form['#after_build']);
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function actions(array $form, FormStateInterface $form_state) {
|
||||
$actions = parent::actions($form, $form_state);
|
||||
$actions['cancel']['#attributes']['class'][] = 'dialog-cancel';
|
||||
return $actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function submitForm(array &$form, FormStateInterface $form_state) {
|
||||
try {
|
||||
$this->workspaceManager->setActiveWorkspace($this->entity);
|
||||
$this->messenger->addMessage($this->t('%workspace_label is now the active workspace.', ['%workspace_label' => $this->entity->label()]));
|
||||
}
|
||||
catch (WorkspaceAccessException $e) {
|
||||
$this->messenger->addError($this->t('You do not have access to activate the %workspace_label workspace.', ['%workspace_label' => $this->entity->label()]));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\workspaces\Form;
|
||||
|
||||
use Drupal\Component\Datetime\TimeInterface;
|
||||
use Drupal\Core\Entity\ContentEntityDeleteForm;
|
||||
use Drupal\Core\Entity\EntityRepositoryInterface;
|
||||
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\workspaces\WorkspaceAssociationInterface;
|
||||
use Drupal\workspaces\WorkspaceRepositoryInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Provides a form for deleting a workspace.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class WorkspaceDeleteForm extends ContentEntityDeleteForm implements WorkspaceFormInterface {
|
||||
|
||||
/**
|
||||
* The workspace entity.
|
||||
*
|
||||
* @var \Drupal\workspaces\WorkspaceInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* The workspace association service.
|
||||
*
|
||||
* @var \Drupal\workspaces\WorkspaceAssociationInterface
|
||||
*/
|
||||
protected $workspaceAssociation;
|
||||
|
||||
/**
|
||||
* The workspace repository service.
|
||||
*
|
||||
* @var \Drupal\workspaces\WorkspaceRepositoryInterface
|
||||
*/
|
||||
protected $workspaceRepository;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('entity.repository'),
|
||||
$container->get('workspaces.association'),
|
||||
$container->get('workspaces.repository'),
|
||||
$container->get('entity_type.bundle.info'),
|
||||
$container->get('datetime.time')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a WorkspaceDeleteForm object.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
|
||||
* The entity repository service.
|
||||
* @param \Drupal\workspaces\WorkspaceAssociationInterface $workspace_association
|
||||
* The workspace association service to check how many revisions will be
|
||||
* deleted.
|
||||
* @param \Drupal\workspaces\WorkspaceRepositoryInterface $workspace_repository
|
||||
* The workspace repository service.
|
||||
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
|
||||
* The entity type bundle service.
|
||||
* @param \Drupal\Component\Datetime\TimeInterface $time
|
||||
* The time service.
|
||||
*/
|
||||
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);
|
||||
$this->workspaceAssociation = $workspace_association;
|
||||
$this->workspaceRepository = $workspace_repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(array $form, FormStateInterface $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());
|
||||
$items = [];
|
||||
foreach (array_keys($tracked_entities) as $entity_type_id => $entity_ids) {
|
||||
$revision_ids = $this->workspaceAssociation->getAssociatedRevisions($this->entity->id(), $entity_type_id, $entity_ids);
|
||||
$label = $this->entityTypeManager->getDefinition($entity_type_id)->getLabel();
|
||||
$items[] = $this->formatPlural(count($revision_ids), '1 @label revision.', '@count @label revisions.', ['@label' => $label]);
|
||||
}
|
||||
$form['revisions'] = [
|
||||
'#theme' => 'item_list',
|
||||
'#title' => $this->t('The following will also be deleted:'),
|
||||
'#items' => $items,
|
||||
];
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getDescription() {
|
||||
return $this->t('This action cannot be undone, and will also delete all content created in this workspace.');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\workspaces\Form;
|
||||
|
||||
use Drupal\Component\Datetime\TimeInterface;
|
||||
use Drupal\Core\Entity\ContentEntityForm;
|
||||
use Drupal\Core\Entity\EntityRepositoryInterface;
|
||||
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Messenger\MessengerInterface;
|
||||
use Drupal\workspaces\WorkspaceAccessException;
|
||||
use Drupal\workspaces\WorkspaceOperationFactory;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Provides the workspace deploy form.
|
||||
*/
|
||||
class WorkspaceDeployForm extends ContentEntityForm implements WorkspaceFormInterface {
|
||||
|
||||
/**
|
||||
* The workspace entity.
|
||||
*
|
||||
* @var \Drupal\workspaces\WorkspaceInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* The messenger service.
|
||||
*
|
||||
* @var \Drupal\Core\Messenger\MessengerInterface
|
||||
*/
|
||||
protected $messenger;
|
||||
|
||||
/**
|
||||
* The workspace operation factory.
|
||||
*
|
||||
* @var \Drupal\workspaces\WorkspaceOperationFactory
|
||||
*/
|
||||
protected $workspaceOperationFactory;
|
||||
|
||||
/**
|
||||
* Constructs a new WorkspaceDeployForm.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
|
||||
* The entity repository service.
|
||||
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
|
||||
* The entity type bundle service.
|
||||
* @param \Drupal\Component\Datetime\TimeInterface $time
|
||||
* The time service.
|
||||
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
|
||||
* The messenger service.
|
||||
* @param \Drupal\workspaces\WorkspaceOperationFactory $workspace_operation_factory
|
||||
* The workspace operation factory service.
|
||||
*/
|
||||
public function __construct(EntityRepositoryInterface $entity_repository, EntityTypeBundleInfoInterface $entity_type_bundle_info, TimeInterface $time, MessengerInterface $messenger, WorkspaceOperationFactory $workspace_operation_factory) {
|
||||
parent::__construct($entity_repository, $entity_type_bundle_info, $time);
|
||||
$this->messenger = $messenger;
|
||||
$this->workspaceOperationFactory = $workspace_operation_factory;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('entity.repository'),
|
||||
$container->get('entity_type.bundle.info'),
|
||||
$container->get('datetime.time'),
|
||||
$container->get('messenger'),
|
||||
$container->get('workspaces.operation_factory')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function form(array $form, FormStateInterface $form_state) {
|
||||
$form = parent::form($form, $form_state);
|
||||
|
||||
$workspace_publisher = $this->workspaceOperationFactory->getPublisher($this->entity);
|
||||
|
||||
$args = [
|
||||
'%source_label' => $this->entity->label(),
|
||||
'%target_label' => $workspace_publisher->getTargetLabel(),
|
||||
];
|
||||
$form['#title'] = $this->t('Deploy %source_label workspace', $args);
|
||||
|
||||
// List the changes that can be pushed.
|
||||
if ($source_rev_diff = $workspace_publisher->getDifferringRevisionIdsOnSource()) {
|
||||
$total_count = $workspace_publisher->getNumberOfChangesOnSource();
|
||||
$form['deploy'] = [
|
||||
'#theme' => 'item_list',
|
||||
'#title' => $this->formatPlural($total_count, 'There is @count item that can be deployed from %source_label to %target_label', 'There are @count items that can be deployed from %source_label to %target_label', $args),
|
||||
'#items' => [],
|
||||
'#total_count' => $total_count,
|
||||
];
|
||||
foreach ($source_rev_diff as $entity_type_id => $revision_difference) {
|
||||
$form['deploy']['#items'][$entity_type_id] = $this->entityTypeManager->getDefinition($entity_type_id)->getCountLabel(count($revision_difference));
|
||||
}
|
||||
}
|
||||
|
||||
// If there are no changes to push or pull, show an informational message.
|
||||
if (!isset($form['deploy']) && !isset($form['refresh'])) {
|
||||
$form['help'] = [
|
||||
'#markup' => $this->t('There are no changes that can be deployed from %source_label to %target_label.', $args),
|
||||
];
|
||||
}
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function actions(array $form, FormStateInterface $form_state) {
|
||||
$elements = parent::actions($form, $form_state);
|
||||
unset($elements['delete']);
|
||||
|
||||
$workspace_publisher = $this->workspaceOperationFactory->getPublisher($this->entity);
|
||||
|
||||
if (isset($form['deploy'])) {
|
||||
$total_count = $form['deploy']['#total_count'];
|
||||
$elements['submit']['#value'] = $this->formatPlural($total_count, 'Deploy @count item to @target', 'Deploy @count items to @target', ['@target' => $workspace_publisher->getTargetLabel()]);
|
||||
$elements['submit']['#submit'] = ['::submitForm', '::deploy'];
|
||||
}
|
||||
else {
|
||||
// Do not allow the 'Deploy' operation if there's nothing to push.
|
||||
$elements['submit']['#value'] = $this->t('Deploy');
|
||||
$elements['submit']['#disabled'] = TRUE;
|
||||
}
|
||||
|
||||
$elements['cancel'] = [
|
||||
'#type' => 'link',
|
||||
'#title' => $this->t('Cancel'),
|
||||
'#attributes' => ['class' => ['button']],
|
||||
'#url' => $this->entity->toUrl('collection'),
|
||||
];
|
||||
|
||||
return $elements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Form submission handler; deploys the content to the workspace's target.
|
||||
*
|
||||
* @param array $form
|
||||
* An associative array containing the structure of the form.
|
||||
* @param \Drupal\Core\Form\FormStateInterface $form_state
|
||||
* The current state of the form.
|
||||
*/
|
||||
public function deploy(array &$form, FormStateInterface $form_state) {
|
||||
$workspace = $this->entity;
|
||||
|
||||
try {
|
||||
$workspace->publish();
|
||||
$this->messenger->addMessage($this->t('Successful deployment.'));
|
||||
}
|
||||
catch (WorkspaceAccessException $e) {
|
||||
$this->messenger->addMessage($e->getMessage(), 'error');
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
$this->messenger->addMessage($this->t('Deployment failed. All errors have been logged.'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\workspaces\Form;
|
||||
|
||||
use Drupal\Component\Datetime\TimeInterface;
|
||||
use Drupal\Core\Entity\ContentEntityForm;
|
||||
use Drupal\Core\Entity\EntityConstraintViolationListInterface;
|
||||
use Drupal\Core\Entity\EntityRepositoryInterface;
|
||||
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Messenger\MessengerInterface;
|
||||
use Drupal\Core\Url;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Form controller for the workspace edit forms.
|
||||
*/
|
||||
class WorkspaceForm extends ContentEntityForm implements WorkspaceFormInterface {
|
||||
|
||||
/**
|
||||
* The workspace entity.
|
||||
*
|
||||
* @var \Drupal\workspaces\WorkspaceInterface
|
||||
*/
|
||||
protected $entity;
|
||||
|
||||
/**
|
||||
* The messenger service.
|
||||
*
|
||||
* @var \Drupal\Core\Messenger\MessengerInterface
|
||||
*/
|
||||
protected $messenger;
|
||||
|
||||
/**
|
||||
* Constructs a new WorkspaceForm.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
|
||||
* The entity repository service.
|
||||
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
|
||||
* The messenger service.
|
||||
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
|
||||
* The entity type bundle service.
|
||||
* @param \Drupal\Component\Datetime\TimeInterface $time
|
||||
* The time service.
|
||||
*/
|
||||
public function __construct(EntityRepositoryInterface $entity_repository, MessengerInterface $messenger, EntityTypeBundleInfoInterface $entity_type_bundle_info = NULL, TimeInterface $time = NULL) {
|
||||
parent::__construct($entity_repository, $entity_type_bundle_info, $time);
|
||||
$this->messenger = $messenger;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('entity.repository'),
|
||||
$container->get('messenger'),
|
||||
$container->get('entity_type.bundle.info'),
|
||||
$container->get('datetime.time')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function form(array $form, FormStateInterface $form_state) {
|
||||
$workspace = $this->entity;
|
||||
|
||||
if ($this->operation == 'edit') {
|
||||
$form['#title'] = $this->t('Edit workspace %label', ['%label' => $workspace->label()]);
|
||||
}
|
||||
$form['label'] = [
|
||||
'#type' => 'textfield',
|
||||
'#title' => $this->t('Label'),
|
||||
'#maxlength' => 255,
|
||||
'#default_value' => $workspace->label(),
|
||||
'#required' => TRUE,
|
||||
];
|
||||
|
||||
$form['id'] = [
|
||||
'#type' => 'machine_name',
|
||||
'#title' => $this->t('Workspace ID'),
|
||||
'#maxlength' => 255,
|
||||
'#default_value' => $workspace->id(),
|
||||
'#disabled' => !$workspace->isNew(),
|
||||
'#machine_name' => [
|
||||
'exists' => '\Drupal\workspaces\Entity\Workspace::load',
|
||||
],
|
||||
'#element_validate' => [],
|
||||
];
|
||||
|
||||
return parent::form($form, $form_state);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getEditedFieldNames(FormStateInterface $form_state) {
|
||||
return array_merge([
|
||||
'label',
|
||||
'id',
|
||||
], parent::getEditedFieldNames($form_state));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function flagViolations(EntityConstraintViolationListInterface $violations, array $form, FormStateInterface $form_state) {
|
||||
// Manually flag violations of fields not handled by the form display. This
|
||||
// is necessary as entity form displays only flag violations for fields
|
||||
// contained in the display.
|
||||
$field_names = [
|
||||
'label',
|
||||
'id',
|
||||
];
|
||||
foreach ($violations->getByFields($field_names) as $violation) {
|
||||
list($field_name) = explode('.', $violation->getPropertyPath(), 2);
|
||||
$form_state->setErrorByName($field_name, $violation->getMessage());
|
||||
}
|
||||
parent::flagViolations($violations, $form, $form_state);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function save(array $form, FormStateInterface $form_state) {
|
||||
$workspace = $this->entity;
|
||||
$workspace->setNewRevision(TRUE);
|
||||
$status = $workspace->save();
|
||||
|
||||
$info = ['%info' => $workspace->label()];
|
||||
$context = ['@type' => $workspace->bundle(), '%info' => $workspace->label()];
|
||||
$logger = $this->logger('workspaces');
|
||||
|
||||
if ($status == SAVED_UPDATED) {
|
||||
$logger->notice('@type: updated %info.', $context);
|
||||
$this->messenger->addMessage($this->t('Workspace %info has been updated.', $info));
|
||||
}
|
||||
else {
|
||||
$logger->notice('@type: added %info.', $context);
|
||||
$this->messenger->addMessage($this->t('Workspace %info has been created.', $info));
|
||||
}
|
||||
|
||||
if ($workspace->id()) {
|
||||
$form_state->setValue('id', $workspace->id());
|
||||
$form_state->set('id', $workspace->id());
|
||||
|
||||
$collection_url = $workspace->toUrl('collection');
|
||||
$redirect = $collection_url->access() ? $collection_url : Url::fromRoute('<front>');
|
||||
$form_state->setRedirectUrl($redirect);
|
||||
}
|
||||
else {
|
||||
$this->messenger->addError($this->t('The workspace could not be saved.'));
|
||||
$form_state->setRebuild();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\workspaces\Form;
|
||||
|
||||
use Drupal\Core\Form\FormInterface;
|
||||
|
||||
/**
|
||||
* Defines interface for workspace forms so they can be easily distinguished.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
interface WorkspaceFormInterface extends FormInterface {}
|
||||
@@ -0,0 +1,153 @@
|
||||
<?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(),
|
||||
]));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\workspaces\Form;
|
||||
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Form\FormBase;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Messenger\MessengerInterface;
|
||||
use Drupal\workspaces\WorkspaceAccessException;
|
||||
use Drupal\workspaces\WorkspaceManagerInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Provides a form that activates a different workspace.
|
||||
*/
|
||||
class WorkspaceSwitcherForm extends FormBase implements WorkspaceFormInterface {
|
||||
|
||||
/**
|
||||
* The workspace manager.
|
||||
*
|
||||
* @var \Drupal\workspaces\WorkspaceManagerInterface
|
||||
*/
|
||||
protected $workspaceManager;
|
||||
|
||||
/**
|
||||
* The workspace entity storage handler.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityStorageInterface
|
||||
*/
|
||||
protected $workspaceStorage;
|
||||
|
||||
/**
|
||||
* The messenger service.
|
||||
*
|
||||
* @var \Drupal\Core\Messenger\MessengerInterface
|
||||
*/
|
||||
protected $messenger;
|
||||
|
||||
/**
|
||||
* Constructs a new WorkspaceSwitcherForm.
|
||||
*
|
||||
* @param \Drupal\workspaces\WorkspaceManagerInterface $workspace_manager
|
||||
* The workspace manager.
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager.
|
||||
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
|
||||
* The messenger service.
|
||||
*/
|
||||
public function __construct(WorkspaceManagerInterface $workspace_manager, EntityTypeManagerInterface $entity_type_manager, MessengerInterface $messenger) {
|
||||
$this->workspaceManager = $workspace_manager;
|
||||
$this->workspaceStorage = $entity_type_manager->getStorage('workspace');
|
||||
$this->messenger = $messenger;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('workspaces.manager'),
|
||||
$container->get('entity_type.manager'),
|
||||
$container->get('messenger')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFormId() {
|
||||
return 'workspace_switcher_form';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(array $form, FormStateInterface $form_state) {
|
||||
$workspaces = $this->workspaceStorage->loadMultiple();
|
||||
$workspace_labels = [];
|
||||
foreach ($workspaces as $workspace) {
|
||||
$workspace_labels[$workspace->id()] = $workspace->label();
|
||||
}
|
||||
|
||||
$active_workspace = $this->workspaceManager->getActiveWorkspace();
|
||||
if ($active_workspace) {
|
||||
unset($workspace_labels[$active_workspace->id()]);
|
||||
}
|
||||
|
||||
$form['current'] = [
|
||||
'#type' => 'item',
|
||||
'#title' => $this->t('Current workspace'),
|
||||
'#markup' => $active_workspace ? $active_workspace->label() : $this->t('None'),
|
||||
'#wrapper_attributes' => [
|
||||
'class' => ['container-inline'],
|
||||
],
|
||||
];
|
||||
|
||||
$form['workspace_id'] = [
|
||||
'#type' => 'select',
|
||||
'#title' => $this->t('Select workspace'),
|
||||
'#required' => TRUE,
|
||||
'#options' => $workspace_labels,
|
||||
'#wrapper_attributes' => [
|
||||
'class' => ['container-inline'],
|
||||
],
|
||||
'#access' => !empty($workspace_labels),
|
||||
];
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function submitForm(array &$form, FormStateInterface $form_state) {
|
||||
$id = $form_state->getValue('workspace_id');
|
||||
|
||||
/** @var \Drupal\workspaces\WorkspaceInterface $workspace */
|
||||
$workspace = $this->workspaceStorage->load($id);
|
||||
|
||||
try {
|
||||
$this->workspaceManager->setActiveWorkspace($workspace);
|
||||
$this->messenger->addMessage($this->t('%workspace_label is now the active workspace.', ['%workspace_label' => $workspace->label()]));
|
||||
}
|
||||
catch (WorkspaceAccessException $e) {
|
||||
$this->messenger->addError($this->t('You do not have access to activate the %workspace_label workspace.', ['%workspace_label' => $workspace->label()]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.'));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\workspaces;
|
||||
|
||||
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Render\Element;
|
||||
use Drupal\Core\StringTranslation\TranslatableMarkup;
|
||||
use Drupal\views\Form\ViewsExposedForm;
|
||||
use Drupal\workspaces\Form\WorkspaceFormInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Defines a class for reacting to form operations.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class FormOperations implements ContainerInjectionInterface {
|
||||
|
||||
/**
|
||||
* The workspace manager service.
|
||||
*
|
||||
* @var \Drupal\workspaces\WorkspaceManagerInterface
|
||||
*/
|
||||
protected $workspaceManager;
|
||||
|
||||
/**
|
||||
* Constructs a new FormOperations instance.
|
||||
*
|
||||
* @param \Drupal\workspaces\WorkspaceManagerInterface $workspace_manager
|
||||
* The workspace manager service.
|
||||
*/
|
||||
public function __construct(WorkspaceManagerInterface $workspace_manager) {
|
||||
$this->workspaceManager = $workspace_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('workspaces.manager')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alters forms to disallow editing in non-default workspaces.
|
||||
*
|
||||
* @param array $form
|
||||
* An associative array containing the structure of the form.
|
||||
* @param \Drupal\Core\Form\FormStateInterface $form_state
|
||||
* The current state of the form.
|
||||
* @param string $form_id
|
||||
* The form ID.
|
||||
*
|
||||
* @see hook_form_alter()
|
||||
*/
|
||||
public function formAlter(array &$form, FormStateInterface $form_state, $form_id) {
|
||||
// No alterations are needed if we're not in a workspace context.
|
||||
if (!$this->workspaceManager->hasActiveWorkspace()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add an additional validation step for every form if we are in a
|
||||
// non-default workspace.
|
||||
$this->addWorkspaceValidation($form);
|
||||
|
||||
// If a form has already been marked as safe or not to submit in a
|
||||
// non-default workspace, we don't have anything else to do.
|
||||
if ($form_state->has('workspace_safe')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// No forms are safe to submit in a non-default workspace by default, except
|
||||
// for the whitelisted ones defined below.
|
||||
$workspace_safe = FALSE;
|
||||
|
||||
// Whitelist a few forms that we know are safe to submit.
|
||||
$form_object = $form_state->getFormObject();
|
||||
$is_workspace_form = $form_object instanceof WorkspaceFormInterface;
|
||||
$is_search_form = in_array($form_object->getFormId(), ['search_block_form', 'search_form'], TRUE);
|
||||
$is_views_exposed_form = $form_object instanceof ViewsExposedForm;
|
||||
if ($is_workspace_form || $is_search_form || $is_views_exposed_form) {
|
||||
$workspace_safe = TRUE;
|
||||
}
|
||||
|
||||
$form_state->set('workspace_safe', $workspace_safe);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds our validation handler recursively on each element of a form.
|
||||
*
|
||||
* @param array &$element
|
||||
* An associative array containing the structure of the form.
|
||||
*/
|
||||
protected function addWorkspaceValidation(array &$element) {
|
||||
// Recurse through all children and add our validation handler if needed.
|
||||
foreach (Element::children($element) as $key) {
|
||||
if (isset($element[$key]) && $element[$key]) {
|
||||
$this->addWorkspaceValidation($element[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($element['#validate'])) {
|
||||
$element['#validate'][] = [get_called_class(), 'validateDefaultWorkspace'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation handler which sets a validation error for all unsupported forms.
|
||||
*/
|
||||
public static function validateDefaultWorkspace(array &$form, FormStateInterface $form_state) {
|
||||
if ($form_state->get('workspace_safe') !== TRUE) {
|
||||
$form_state->setError($form, new TranslatableMarkup('This form can only be submitted in the default workspace.'));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\workspaces\Negotiator;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
/**
|
||||
* Defines the query parameter workspace negotiator.
|
||||
*/
|
||||
class QueryParameterWorkspaceNegotiator extends SessionWorkspaceNegotiator {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function applies(Request $request) {
|
||||
return is_string($request->query->get('workspace')) && parent::applies($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getActiveWorkspace(Request $request) {
|
||||
$workspace_id = $request->query->get('workspace');
|
||||
|
||||
if ($workspace_id && ($workspace = $this->workspaceStorage->load($workspace_id))) {
|
||||
$this->setActiveWorkspace($workspace);
|
||||
return $workspace;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\workspaces\Negotiator;
|
||||
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\workspaces\WorkspaceInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Session\Session;
|
||||
|
||||
/**
|
||||
* Defines the session workspace negotiator.
|
||||
*/
|
||||
class SessionWorkspaceNegotiator implements WorkspaceNegotiatorInterface {
|
||||
|
||||
/**
|
||||
* The current user.
|
||||
*
|
||||
* @var \Drupal\Core\Session\AccountInterface
|
||||
*/
|
||||
protected $currentUser;
|
||||
|
||||
/**
|
||||
* The session.
|
||||
*
|
||||
* @var \Symfony\Component\HttpFoundation\Session\Session
|
||||
*/
|
||||
protected $session;
|
||||
|
||||
/**
|
||||
* The workspace storage handler.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityStorageInterface
|
||||
*/
|
||||
protected $workspaceStorage;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param \Drupal\Core\Session\AccountInterface $current_user
|
||||
* The current user.
|
||||
* @param \Symfony\Component\HttpFoundation\Session\Session $session
|
||||
* The session.
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager.
|
||||
*/
|
||||
public function __construct(AccountInterface $current_user, Session $session, EntityTypeManagerInterface $entity_type_manager) {
|
||||
$this->currentUser = $current_user;
|
||||
$this->session = $session;
|
||||
$this->workspaceStorage = $entity_type_manager->getStorage('workspace');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function applies(Request $request) {
|
||||
// This negotiator only applies if the current user is authenticated.
|
||||
return $this->currentUser->isAuthenticated();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getActiveWorkspace(Request $request) {
|
||||
$workspace_id = $this->session->get('active_workspace_id');
|
||||
|
||||
if ($workspace_id && ($workspace = $this->workspaceStorage->load($workspace_id))) {
|
||||
return $workspace;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setActiveWorkspace(WorkspaceInterface $workspace) {
|
||||
$this->session->set('active_workspace_id', $workspace->id());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function unsetActiveWorkspace() {
|
||||
$this->session->remove('active_workspace_id');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\workspaces\Negotiator;
|
||||
|
||||
use Drupal\workspaces\WorkspaceInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
/**
|
||||
* Workspace negotiators provide a way to get the active workspace.
|
||||
*
|
||||
* \Drupal\workspaces\WorkspaceManager acts as the service collector for
|
||||
* Workspace negotiators.
|
||||
*/
|
||||
interface WorkspaceNegotiatorInterface {
|
||||
|
||||
/**
|
||||
* Checks whether the negotiator applies to the current request or not.
|
||||
*
|
||||
* @param \Symfony\Component\HttpFoundation\Request $request
|
||||
* The HTTP request.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the negotiator applies for the current request, FALSE otherwise.
|
||||
*/
|
||||
public function applies(Request $request);
|
||||
|
||||
/**
|
||||
* Gets the negotiated workspace, if any.
|
||||
*
|
||||
* Note that it is the responsibility of each implementation to check whether
|
||||
* the negotiated workspace actually exists in the storage.
|
||||
*
|
||||
* @param \Symfony\Component\HttpFoundation\Request $request
|
||||
* The HTTP request.
|
||||
*
|
||||
* @return \Drupal\workspaces\WorkspaceInterface|null
|
||||
* The negotiated workspace or NULL if the negotiator could not determine a
|
||||
* valid workspace.
|
||||
*/
|
||||
public function getActiveWorkspace(Request $request);
|
||||
|
||||
/**
|
||||
* Sets the negotiated workspace.
|
||||
*
|
||||
* @param \Drupal\workspaces\WorkspaceInterface $workspace
|
||||
* The workspace entity.
|
||||
*/
|
||||
public function setActiveWorkspace(WorkspaceInterface $workspace);
|
||||
|
||||
/**
|
||||
* Unsets the negotiated workspace.
|
||||
*/
|
||||
public function unsetActiveWorkspace();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\workspaces\Plugin\Block;
|
||||
|
||||
use Drupal\Core\Block\BlockBase;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Form\FormBuilderInterface;
|
||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||
use Drupal\workspaces\Form\WorkspaceSwitcherForm;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Provides a 'Workspace switcher' block.
|
||||
*
|
||||
* @Block(
|
||||
* id = "workspace_switcher",
|
||||
* admin_label = @Translation("Workspace switcher"),
|
||||
* category = @Translation("Workspace"),
|
||||
* )
|
||||
*/
|
||||
class WorkspaceSwitcherBlock extends BlockBase implements ContainerFactoryPluginInterface {
|
||||
|
||||
/**
|
||||
* The form builder.
|
||||
*
|
||||
* @var \Drupal\Core\Form\FormBuilderInterface
|
||||
*/
|
||||
protected $formBuilder;
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* Constructs a new WorkspaceSwitcherBlock instance.
|
||||
*
|
||||
* @param array $configuration
|
||||
* The plugin configuration.
|
||||
* @param string $plugin_id
|
||||
* The plugin ID.
|
||||
* @param mixed $plugin_definition
|
||||
* The plugin definition.
|
||||
* @param \Drupal\Core\Form\FormBuilderInterface $form_builder
|
||||
* The form builder.
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager.
|
||||
*/
|
||||
public function __construct(array $configuration, $plugin_id, $plugin_definition, FormBuilderInterface $form_builder, EntityTypeManagerInterface $entity_type_manager) {
|
||||
parent::__construct($configuration, $plugin_id, $plugin_definition);
|
||||
$this->formBuilder = $form_builder;
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
|
||||
return new static(
|
||||
$configuration,
|
||||
$plugin_id,
|
||||
$plugin_definition,
|
||||
$container->get('form_builder'),
|
||||
$container->get('entity_type.manager')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function build() {
|
||||
$build = [
|
||||
'form' => $this->formBuilder->getForm(WorkspaceSwitcherForm::class),
|
||||
'#cache' => [
|
||||
'contexts' => $this->entityTypeManager->getDefinition('workspace')->getListCacheContexts(),
|
||||
'tags' => $this->entityTypeManager->getDefinition('workspace')->getListCacheTags(),
|
||||
],
|
||||
];
|
||||
return $build;
|
||||
}
|
||||
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
<?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;
|
||||
}
|
||||
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\workspaces\Plugin\Validation\Constraint;
|
||||
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
|
||||
/**
|
||||
* Deleted workspace constraint.
|
||||
*
|
||||
* @Constraint(
|
||||
* id = "DeletedWorkspace",
|
||||
* label = @Translation("Deleted workspace", context = "Validation"),
|
||||
* )
|
||||
*/
|
||||
class DeletedWorkspaceConstraint extends Constraint {
|
||||
|
||||
/**
|
||||
* The default violation message.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $message = 'A workspace with this ID has been deleted but data still exists for it.';
|
||||
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\workspaces\Plugin\Validation\Constraint;
|
||||
|
||||
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
||||
use Drupal\workspaces\WorkspaceAssociationInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
|
||||
/**
|
||||
* Checks if data still exists for a deleted workspace ID.
|
||||
*/
|
||||
class DeletedWorkspaceConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
|
||||
|
||||
/**
|
||||
* The workspace association service.
|
||||
*
|
||||
* @var \Drupal\workspaces\WorkspaceAssociationInterface
|
||||
*/
|
||||
protected $workspaceAssociation;
|
||||
|
||||
/**
|
||||
* Creates a new DeletedWorkspaceConstraintValidator instance.
|
||||
*
|
||||
* @param \Drupal\workspaces\WorkspaceAssociationInterface $workspace_association
|
||||
* The workspace association service.
|
||||
*/
|
||||
public function __construct(WorkspaceAssociationInterface $workspace_association) {
|
||||
$this->workspaceAssociation = $workspace_association;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('workspaces.association')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validate($value, Constraint $constraint) {
|
||||
/** @var \Drupal\Core\Field\FieldItemListInterface $value */
|
||||
// This constraint applies only to newly created workspace entities.
|
||||
if (!isset($value) || !$value->getEntity()->isNew()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->workspaceAssociation->getTrackedEntities($value->getEntity()->id())) {
|
||||
$this->context->addViolation($constraint->message);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\workspaces\Plugin\Validation\Constraint;
|
||||
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
|
||||
/**
|
||||
* The entity reference supported new entities constraint.
|
||||
*
|
||||
* @Constraint(
|
||||
* id = "EntityReferenceSupportedNewEntities",
|
||||
* label = @Translation("Entity Reference Supported New Entities", context = "Validation"),
|
||||
* )
|
||||
*/
|
||||
class EntityReferenceSupportedNewEntitiesConstraint extends Constraint {
|
||||
|
||||
/**
|
||||
* The default violation message.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $message = '%collection_label can only be created in the default workspace.';
|
||||
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\workspaces\Plugin\Validation\Constraint;
|
||||
|
||||
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\workspaces\WorkspaceManagerInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
|
||||
/**
|
||||
* Checks if new entities created for entity reference fields are supported.
|
||||
*/
|
||||
class EntityReferenceSupportedNewEntitiesConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
|
||||
|
||||
/**
|
||||
* The workspace manager.
|
||||
*
|
||||
* @var \Drupal\workspaces\WorkspaceManagerInterface
|
||||
*/
|
||||
protected $workspaceManager;
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* Creates a new EntityReferenceSupportedNewEntitiesConstraintValidator instance.
|
||||
*/
|
||||
public function __construct(WorkspaceManagerInterface $workspaceManager, EntityTypeManagerInterface $entityTypeManager) {
|
||||
$this->workspaceManager = $workspaceManager;
|
||||
$this->entityTypeManager = $entityTypeManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('workspaces.manager'),
|
||||
$container->get('entity_type.manager')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validate($value, Constraint $constraint) {
|
||||
// The validator should run only if we are in a active workspace context.
|
||||
if (!$this->workspaceManager->hasActiveWorkspace()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$target_entity_type_id = $value->getFieldDefinition()->getFieldStorageDefinition()->getSetting('target_type');
|
||||
$target_entity_type = $this->entityTypeManager->getDefinition($target_entity_type_id);
|
||||
|
||||
if ($value->hasNewEntity() && !$this->workspaceManager->isEntityTypeSupported($target_entity_type)) {
|
||||
$this->context->addViolation($constraint->message, ['%collection_label' => $target_entity_type->getCollectionLabel()]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\workspaces\Plugin\Validation\Constraint;
|
||||
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
|
||||
/**
|
||||
* Validation constraint for an entity being edited in multiple workspaces.
|
||||
*
|
||||
* @Constraint(
|
||||
* id = "EntityWorkspaceConflict",
|
||||
* label = @Translation("Entity workspace conflict", context = "Validation"),
|
||||
* type = {"entity"}
|
||||
* )
|
||||
*/
|
||||
class EntityWorkspaceConflictConstraint extends Constraint {
|
||||
|
||||
/**
|
||||
* The default violation message.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $message = 'The content is being edited in the %label workspace. As a result, your changes cannot be saved.';
|
||||
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\workspaces\Plugin\Validation\Constraint;
|
||||
|
||||
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\workspaces\WorkspaceAssociationInterface;
|
||||
use Drupal\workspaces\WorkspaceManagerInterface;
|
||||
use Drupal\workspaces\WorkspaceRepositoryInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
|
||||
/**
|
||||
* Validates the EntityWorkspaceConflict constraint.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('entity_type.manager'),
|
||||
$container->get('workspaces.manager'),
|
||||
$container->get('workspaces.association'),
|
||||
$container->get('workspaces.repository')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validate($entity, Constraint $constraint) {
|
||||
/** @var \Drupal\Core\Entity\EntityInterface $entity */
|
||||
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) {
|
||||
$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 (!$active_workspace || !in_array($active_workspace->id(), $descendants_and_self, TRUE)) {
|
||||
$this->context->buildViolation($constraint->message)
|
||||
->setParameter('%label', $latest_revision_workspace->label())
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,441 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\workspaces;
|
||||
|
||||
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
||||
use Drupal\Core\Entity\EntityFieldManagerInterface;
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Language\LanguageManagerInterface;
|
||||
use Drupal\views\Plugin\views\query\QueryPluginBase;
|
||||
use Drupal\views\Plugin\views\query\Sql;
|
||||
use Drupal\views\Plugin\ViewsHandlerManager;
|
||||
use Drupal\views\ViewExecutable;
|
||||
use Drupal\views\ViewsData;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Defines a class for altering views queries.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class ViewsQueryAlter implements ContainerInjectionInterface {
|
||||
|
||||
/**
|
||||
* The entity type manager service.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The entity field manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
|
||||
*/
|
||||
protected $entityFieldManager;
|
||||
|
||||
/**
|
||||
* The workspace manager service.
|
||||
*
|
||||
* @var \Drupal\workspaces\WorkspaceManagerInterface
|
||||
*/
|
||||
protected $workspaceManager;
|
||||
|
||||
/**
|
||||
* The views data.
|
||||
*
|
||||
* @var \Drupal\views\ViewsData
|
||||
*/
|
||||
protected $viewsData;
|
||||
|
||||
/**
|
||||
* A plugin manager which handles instances of views join plugins.
|
||||
*
|
||||
* @var \Drupal\views\Plugin\ViewsHandlerManager
|
||||
*/
|
||||
protected $viewsJoinPluginManager;
|
||||
|
||||
/**
|
||||
* The language manager.
|
||||
*
|
||||
* @var \Drupal\Core\Language\LanguageManagerInterface
|
||||
*/
|
||||
protected $languageManager;
|
||||
|
||||
/**
|
||||
* Constructs a new ViewsQueryAlter instance.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager service.
|
||||
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
|
||||
* The entity field manager.
|
||||
* @param \Drupal\workspaces\WorkspaceManagerInterface $workspace_manager
|
||||
* The workspace manager service.
|
||||
* @param \Drupal\views\ViewsData $views_data
|
||||
* The views data.
|
||||
* @param \Drupal\views\Plugin\ViewsHandlerManager $views_join_plugin_manager
|
||||
* The views join plugin manager.
|
||||
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
|
||||
* The language manager.
|
||||
*/
|
||||
public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, WorkspaceManagerInterface $workspace_manager, ViewsData $views_data, ViewsHandlerManager $views_join_plugin_manager, LanguageManagerInterface $language_manager) {
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->entityFieldManager = $entity_field_manager;
|
||||
$this->workspaceManager = $workspace_manager;
|
||||
$this->viewsData = $views_data;
|
||||
$this->viewsJoinPluginManager = $views_join_plugin_manager;
|
||||
$this->languageManager = $language_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('entity_type.manager'),
|
||||
$container->get('entity_field.manager'),
|
||||
$container->get('workspaces.manager'),
|
||||
$container->get('views.views_data'),
|
||||
$container->get('plugin.manager.views.join'),
|
||||
$container->get('language_manager')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a hook bridge for hook_views_query_alter().
|
||||
*
|
||||
* @see hook_views_query_alter()
|
||||
*/
|
||||
public function alterQuery(ViewExecutable $view, QueryPluginBase $query) {
|
||||
// Don't alter any views queries if we're not in a workspace context.
|
||||
if (!$this->workspaceManager->hasActiveWorkspace()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't alter any non-sql views queries.
|
||||
if (!$query instanceof Sql) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find out what entity types are represented in this query.
|
||||
$entity_type_ids = [];
|
||||
foreach ($query->relationships as $info) {
|
||||
$table_data = $this->viewsData->get($info['base']);
|
||||
if (empty($table_data['table']['entity type'])) {
|
||||
continue;
|
||||
}
|
||||
$entity_type_id = $table_data['table']['entity type'];
|
||||
// This construct ensures each entity type exists only once.
|
||||
$entity_type_ids[$entity_type_id] = $entity_type_id;
|
||||
}
|
||||
|
||||
$entity_type_definitions = $this->entityTypeManager->getDefinitions();
|
||||
foreach ($entity_type_ids as $entity_type_id) {
|
||||
if ($this->workspaceManager->isEntityTypeSupported($entity_type_definitions[$entity_type_id])) {
|
||||
$this->alterQueryForEntityType($query, $entity_type_definitions[$entity_type_id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Alters the entity type tables for a Views query.
|
||||
*
|
||||
* This should only be called after determining that this entity type is
|
||||
* involved in the query, and that a non-default workspace is in use.
|
||||
*
|
||||
* @param \Drupal\views\Plugin\views\query\Sql $query
|
||||
* The query plugin object for the query.
|
||||
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
|
||||
* The entity type definition.
|
||||
*/
|
||||
protected function alterQueryForEntityType(Sql $query, EntityTypeInterface $entity_type) {
|
||||
/** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
|
||||
$table_mapping = $this->entityTypeManager->getStorage($entity_type->id())->getTableMapping();
|
||||
$field_storage_definitions = $this->entityFieldManager->getFieldStorageDefinitions($entity_type->id());
|
||||
$dedicated_field_storage_definitions = array_filter($field_storage_definitions, function ($definition) use ($table_mapping) {
|
||||
return $table_mapping->requiresDedicatedTableStorage($definition);
|
||||
});
|
||||
$dedicated_field_data_tables = array_map(function ($definition) use ($table_mapping) {
|
||||
return $table_mapping->getDedicatedDataTableName($definition);
|
||||
}, $dedicated_field_storage_definitions);
|
||||
|
||||
$move_workspace_tables = [];
|
||||
$table_queue =& $query->getTableQueue();
|
||||
foreach ($table_queue as $alias => &$table_info) {
|
||||
// If we reach the workspace_association array item before any candidates,
|
||||
// then we do not need to move it.
|
||||
if ($table_info['table'] == 'workspace_association') {
|
||||
break;
|
||||
}
|
||||
|
||||
// Any dedicated field table is a candidate.
|
||||
if ($field_name = array_search($table_info['table'], $dedicated_field_data_tables, TRUE)) {
|
||||
$relationship = $table_info['relationship'];
|
||||
|
||||
// There can be reverse relationships used. If so, Workspaces can't do
|
||||
// anything with them. Detect this and skip.
|
||||
if ($table_info['join']->field != 'entity_id') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the dedicated revision table name.
|
||||
$new_table_name = $table_mapping->getDedicatedRevisionTableName($field_storage_definitions[$field_name]);
|
||||
|
||||
// Now add the workspace_association table.
|
||||
$workspace_association_table = $this->ensureWorkspaceAssociationTable($entity_type->id(), $query, $relationship);
|
||||
|
||||
// Update the join to use our COALESCE.
|
||||
$revision_field = $entity_type->getKey('revision');
|
||||
$table_info['join']->leftFormula = "COALESCE($workspace_association_table.target_entity_revision_id, $relationship.$revision_field)";
|
||||
|
||||
// Update the join and the table info to our new table name, and to join
|
||||
// on the revision key.
|
||||
$table_info['table'] = $new_table_name;
|
||||
$table_info['join']->table = $new_table_name;
|
||||
$table_info['join']->field = 'revision_id';
|
||||
|
||||
// Finally, if we added the workspace_association table we have to move
|
||||
// it in the table queue so that it comes before this field.
|
||||
if (empty($move_workspace_tables[$workspace_association_table])) {
|
||||
$move_workspace_tables[$workspace_association_table] = $alias;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// JOINs must be in order. i.e, any tables you mention in the ON clause of a
|
||||
// JOIN must appear prior to that JOIN. Since we're modifying a JOIN in
|
||||
// place, and adding a new table, we must ensure that the new table appears
|
||||
// prior to this one. So we recorded at what index we saw that table, and
|
||||
// then use array_splice() to move the workspace_association table join to
|
||||
// the correct position.
|
||||
foreach ($move_workspace_tables as $workspace_association_table => $alias) {
|
||||
$this->moveEntityTable($query, $workspace_association_table, $alias);
|
||||
}
|
||||
|
||||
$base_entity_table = $entity_type->isTranslatable() ? $entity_type->getDataTable() : $entity_type->getBaseTable();
|
||||
|
||||
$base_fields = array_diff($table_mapping->getFieldNames($entity_type->getBaseTable()), [$entity_type->getKey('langcode')]);
|
||||
$revisionable_fields = array_diff($table_mapping->getFieldNames($entity_type->getRevisionDataTable()), $base_fields);
|
||||
|
||||
// Go through and look to see if we have to modify fields and filters.
|
||||
foreach ($query->fields as &$field_info) {
|
||||
// Some fields don't actually have tables, meaning they're formulae and
|
||||
// whatnot. At this time we are going to ignore those.
|
||||
if (empty($field_info['table'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Dereference the alias into the actual table.
|
||||
$table = $table_queue[$field_info['table']]['table'];
|
||||
if ($table == $base_entity_table && in_array($field_info['field'], $revisionable_fields)) {
|
||||
$relationship = $table_queue[$field_info['table']]['alias'];
|
||||
$alias = $this->ensureRevisionTable($entity_type, $query, $relationship);
|
||||
if ($alias) {
|
||||
// Change the base table to use the revision table instead.
|
||||
$field_info['table'] = $alias;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$relationships = [];
|
||||
// Build a list of all relationships that might be for our table.
|
||||
foreach ($query->relationships as $relationship => $info) {
|
||||
if ($info['base'] == $base_entity_table) {
|
||||
$relationships[] = $relationship;
|
||||
}
|
||||
}
|
||||
|
||||
// Now we have to go through our where clauses and modify any of our fields.
|
||||
foreach ($query->where as &$clauses) {
|
||||
foreach ($clauses['conditions'] as &$where_info) {
|
||||
// Build a matrix of our possible relationships against fields we need
|
||||
// to switch.
|
||||
foreach ($relationships as $relationship) {
|
||||
foreach ($revisionable_fields as $field) {
|
||||
if (is_string($where_info['field']) && $where_info['field'] == "$relationship.$field") {
|
||||
$alias = $this->ensureRevisionTable($entity_type, $query, $relationship);
|
||||
if ($alias) {
|
||||
// Change the base table to use the revision table instead.
|
||||
$where_info['field'] = "$alias.$field";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @todo Handle $query->orderby, $query->groupby, $query->having and
|
||||
// $query->count_field in https://www.drupal.org/node/2968165.
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the 'workspace_association' table to a views query.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The ID of the entity type to join.
|
||||
* @param \Drupal\views\Plugin\views\query\Sql $query
|
||||
* The query plugin object for the query.
|
||||
* @param string $relationship
|
||||
* The primary table alias this table is related to.
|
||||
*
|
||||
* @return string
|
||||
* The alias of the 'workspace_association' table.
|
||||
*/
|
||||
protected function ensureWorkspaceAssociationTable($entity_type_id, Sql $query, $relationship) {
|
||||
if (isset($query->tables[$relationship]['workspace_association'])) {
|
||||
return $query->tables[$relationship]['workspace_association']['alias'];
|
||||
}
|
||||
|
||||
$table_data = $this->viewsData->get($query->relationships[$relationship]['base']);
|
||||
|
||||
// Construct the join.
|
||||
$definition = [
|
||||
'table' => 'workspace_association',
|
||||
'field' => 'target_entity_id',
|
||||
'left_table' => $relationship,
|
||||
'left_field' => $table_data['table']['base']['field'],
|
||||
'extra' => [
|
||||
[
|
||||
'field' => 'target_entity_type_id',
|
||||
'value' => $entity_type_id,
|
||||
],
|
||||
[
|
||||
'field' => 'workspace',
|
||||
'value' => $this->workspaceManager->getActiveWorkspace()->id(),
|
||||
],
|
||||
],
|
||||
'type' => 'LEFT',
|
||||
];
|
||||
|
||||
$join = $this->viewsJoinPluginManager->createInstance('standard', $definition);
|
||||
$join->adjusted = TRUE;
|
||||
|
||||
return $query->queueTable('workspace_association', $relationship, $join);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the revision table of an entity type to a query object.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
|
||||
* The entity type definition.
|
||||
* @param \Drupal\views\Plugin\views\query\Sql $query
|
||||
* The query plugin object for the query.
|
||||
* @param string $relationship
|
||||
* The name of the relationship.
|
||||
*
|
||||
* @return string
|
||||
* The alias of the relationship.
|
||||
*/
|
||||
protected function ensureRevisionTable(EntityTypeInterface $entity_type, Sql $query, $relationship) {
|
||||
// Get the alias for the 'workspace_association' table we chain off of in
|
||||
// the COALESCE.
|
||||
$workspace_association_table = $this->ensureWorkspaceAssociationTable($entity_type->id(), $query, $relationship);
|
||||
|
||||
// Get the name of the revision table and revision key.
|
||||
$base_revision_table = $entity_type->isTranslatable() ? $entity_type->getRevisionDataTable() : $entity_type->getRevisionTable();
|
||||
$revision_field = $entity_type->getKey('revision');
|
||||
|
||||
// If the table was already added and has a join against the same field on
|
||||
// the revision table, reuse that rather than adding a new join.
|
||||
if (isset($query->tables[$relationship][$base_revision_table])) {
|
||||
$table_queue =& $query->getTableQueue();
|
||||
$alias = $query->tables[$relationship][$base_revision_table]['alias'];
|
||||
if (isset($table_queue[$alias]['join']->field) && $table_queue[$alias]['join']->field == $revision_field) {
|
||||
// If this table previously existed, but was not added by us, we need
|
||||
// to modify the join and make sure that 'workspace_association' comes
|
||||
// first.
|
||||
if (empty($table_queue[$alias]['join']->workspace_adjusted)) {
|
||||
$table_queue[$alias]['join'] = $this->getRevisionTableJoin($relationship, $base_revision_table, $revision_field, $workspace_association_table, $entity_type);
|
||||
// We also have to ensure that our 'workspace_association' comes before
|
||||
// this.
|
||||
$this->moveEntityTable($query, $workspace_association_table, $alias);
|
||||
}
|
||||
|
||||
return $alias;
|
||||
}
|
||||
}
|
||||
|
||||
// Construct a new join.
|
||||
$join = $this->getRevisionTableJoin($relationship, $base_revision_table, $revision_field, $workspace_association_table, $entity_type);
|
||||
return $query->queueTable($base_revision_table, $relationship, $join);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a join for a revision table using the workspace_association table.
|
||||
*
|
||||
* @param string $relationship
|
||||
* The relationship to use in the view.
|
||||
* @param string $table
|
||||
* The table name.
|
||||
* @param string $field
|
||||
* The field to join on.
|
||||
* @param string $workspace_association_table
|
||||
* The alias of the 'workspace_association' table joined to the main entity
|
||||
* table.
|
||||
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
|
||||
* The entity type that is being queried.
|
||||
*
|
||||
* @return \Drupal\views\Plugin\views\join\JoinPluginInterface
|
||||
* An adjusted views join object to add to the query.
|
||||
* @throws \Drupal\Component\Plugin\Exception\PluginException
|
||||
*/
|
||||
protected function getRevisionTableJoin($relationship, $table, $field, $workspace_association_table, EntityTypeInterface $entity_type) {
|
||||
$definition = [
|
||||
'table' => $table,
|
||||
'field' => $field,
|
||||
// Making this explicitly NULL allows the left table to be a formula.
|
||||
'left_table' => NULL,
|
||||
'left_field' => "COALESCE($workspace_association_table.target_entity_revision_id, $relationship.$field)",
|
||||
];
|
||||
|
||||
if ($entity_type->isTranslatable() && $this->languageManager->isMultilingual()) {
|
||||
$langcode_field = $entity_type->getKey('langcode');
|
||||
$definition['extra'] = "$table.$langcode_field = $relationship.$langcode_field";
|
||||
}
|
||||
|
||||
/** @var \Drupal\views\Plugin\views\join\JoinPluginInterface $join */
|
||||
$join = $this->viewsJoinPluginManager->createInstance('standard', $definition);
|
||||
$join->adjusted = TRUE;
|
||||
$join->workspace_adjusted = TRUE;
|
||||
|
||||
return $join;
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves a 'workspace_association' table to appear before the given alias.
|
||||
*
|
||||
* Because Workspace chains possibly pre-existing tables onto the
|
||||
* 'workspace_association' table, we have to ensure that the
|
||||
* 'workspace_association' table appears in the query before the alias it's
|
||||
* chained on or the SQL is invalid.
|
||||
*
|
||||
* @param \Drupal\views\Plugin\views\query\Sql $query
|
||||
* The SQL query object.
|
||||
* @param string $workspace_association_table
|
||||
* The alias of the 'workspace_association' table.
|
||||
* @param string $alias
|
||||
* The alias of the table it needs to appear before.
|
||||
*/
|
||||
protected function moveEntityTable(Sql $query, $workspace_association_table, $alias) {
|
||||
$table_queue =& $query->getTableQueue();
|
||||
$keys = array_keys($table_queue);
|
||||
$current_index = array_search($workspace_association_table, $keys);
|
||||
$index = array_search($alias, $keys);
|
||||
|
||||
// If it's already before our table, we don't need to move it, as we could
|
||||
// accidentally move it forward.
|
||||
if ($current_index < $index) {
|
||||
return;
|
||||
}
|
||||
$splice = [$workspace_association_table => $table_queue[$workspace_association_table]];
|
||||
unset($table_queue[$workspace_association_table]);
|
||||
|
||||
// Now move the item to the proper location in the array. Don't use
|
||||
// array_splice() because that breaks indices.
|
||||
$table_queue = array_slice($table_queue, 0, $index, TRUE) +
|
||||
$splice +
|
||||
array_slice($table_queue, $index, NULL, TRUE);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\workspaces;
|
||||
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
use Drupal\Core\Entity\EntityAccessControlHandler;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
|
||||
/**
|
||||
* Defines the access control handler for the workspace entity type.
|
||||
*
|
||||
* @see \Drupal\workspaces\Entity\Workspace
|
||||
*/
|
||||
class WorkspaceAccessControlHandler extends EntityAccessControlHandler {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
|
||||
/** @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')) {
|
||||
return AccessResult::allowed()->cachePerPermissions();
|
||||
}
|
||||
|
||||
// @todo Consider adding explicit "publish any|own workspace" permissions in
|
||||
// https://www.drupal.org/project/drupal/issues/3084260.
|
||||
$permission_operation = ($operation === 'update' || $operation === 'publish') ? 'edit' : $operation;
|
||||
|
||||
// Check if the user has permission to access all workspaces.
|
||||
$access_result = AccessResult::allowedIfHasPermission($account, $permission_operation . ' any workspace');
|
||||
|
||||
// Check if it's their own workspace, and they have permission to access
|
||||
// their own workspace.
|
||||
if ($access_result->isNeutral() && $account->isAuthenticated() && $account->id() === $entity->getOwnerId()) {
|
||||
$access_result = AccessResult::allowedIfHasPermission($account, $permission_operation . ' own workspace')
|
||||
->cachePerUser()
|
||||
->addCacheableDependency($entity);
|
||||
}
|
||||
|
||||
return $access_result;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
|
||||
return AccessResult::allowedIfHasPermission($account, 'create workspace');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\workspaces;
|
||||
|
||||
use Drupal\Core\Access\AccessException;
|
||||
|
||||
/**
|
||||
* Exception thrown when trying to switch to an inaccessible workspace.
|
||||
*/
|
||||
class WorkspaceAccessException extends AccessException {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\workspaces;
|
||||
|
||||
use Drupal\Core\Database\Connection;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Entity\RevisionableInterface;
|
||||
use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
|
||||
|
||||
/**
|
||||
* Provides a class for CRUD operations on workspace associations.
|
||||
*/
|
||||
class WorkspaceAssociation implements WorkspaceAssociationInterface {
|
||||
|
||||
/**
|
||||
* The table for the workspace association storage.
|
||||
*/
|
||||
const TABLE = 'workspace_association';
|
||||
|
||||
/**
|
||||
* The database connection.
|
||||
*
|
||||
* @var \Drupal\Core\Database\Connection
|
||||
*/
|
||||
protected $database;
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The workspace repository service.
|
||||
*
|
||||
* @var \Drupal\workspaces\WorkspaceRepositoryInterface
|
||||
*/
|
||||
protected $workspaceRepository;
|
||||
|
||||
/**
|
||||
* Constructs a WorkspaceAssociation object.
|
||||
*
|
||||
* @param \Drupal\Core\Database\Connection $connection
|
||||
* A database connection for reading and writing path aliases.
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager for querying revisions.
|
||||
* @param \Drupal\workspaces\WorkspaceRepositoryInterface $workspace_repository
|
||||
* The Workspace repository service.
|
||||
*/
|
||||
public function __construct(Connection $connection, EntityTypeManagerInterface $entity_type_manager, WorkspaceRepositoryInterface $workspace_repository) {
|
||||
$this->database = $connection;
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->workspaceRepository = $workspace_repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function trackEntity(RevisionableInterface $entity, WorkspaceInterface $workspace) {
|
||||
// Determine all workspaces that might be affected by this change.
|
||||
$affected_workspaces = $this->workspaceRepository->getDescendantsAndSelf($workspace->id());
|
||||
|
||||
// Get the currently tracked revision for this workspace.
|
||||
$tracked = $this->getTrackedEntities($workspace->id(), $entity->getEntityTypeId(), [$entity->id()]);
|
||||
|
||||
$tracked_revision_id = NULL;
|
||||
if (isset($tracked[$entity->getEntityTypeId()])) {
|
||||
$tracked_revision_id = key($tracked[$entity->getEntityTypeId()]);
|
||||
}
|
||||
|
||||
$transaction = $this->database->startTransaction();
|
||||
try {
|
||||
// Update all affected workspaces that were tracking the current revision.
|
||||
// This means they are inheriting content and should be updated.
|
||||
if ($tracked_revision_id) {
|
||||
$this->database->update(static::TABLE)
|
||||
->fields([
|
||||
'target_entity_revision_id' => $entity->getRevisionId(),
|
||||
])
|
||||
->condition('workspace', $affected_workspaces, 'IN')
|
||||
->condition('target_entity_type_id', $entity->getEntityTypeId())
|
||||
->condition('target_entity_id', $entity->id())
|
||||
// Only update descendant workspaces if they have the same initial
|
||||
// revision, which means they are currently inheriting content.
|
||||
->condition('target_entity_revision_id', $tracked_revision_id)
|
||||
->execute();
|
||||
}
|
||||
|
||||
// Insert a new index entry for each workspace that is not tracking this
|
||||
// entity yet.
|
||||
$missing_workspaces = array_diff($affected_workspaces, $this->getEntityTrackingWorkspaceIds($entity));
|
||||
if ($missing_workspaces) {
|
||||
$insert_query = $this->database->insert(static::TABLE)
|
||||
->fields([
|
||||
'workspace',
|
||||
'target_entity_revision_id',
|
||||
'target_entity_type_id',
|
||||
'target_entity_id',
|
||||
]);
|
||||
foreach ($missing_workspaces as $workspace_id) {
|
||||
$insert_query->values([
|
||||
'workspace' => $workspace_id,
|
||||
'target_entity_type_id' => $entity->getEntityTypeId(),
|
||||
'target_entity_id' => $entity->id(),
|
||||
'target_entity_revision_id' => $entity->getRevisionId(),
|
||||
]);
|
||||
}
|
||||
$insert_query->execute();
|
||||
}
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
$transaction->rollBack();
|
||||
watchdog_exception('workspaces', $e);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function workspaceInsert(WorkspaceInterface $workspace) {
|
||||
// When a new workspace has been saved, we need to copy all the associations
|
||||
// of its parent.
|
||||
if ($workspace->hasParent()) {
|
||||
$this->initializeWorkspace($workspace);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getTrackedEntities($workspace_id, $entity_type_id = NULL, $entity_ids = NULL) {
|
||||
$query = $this->database->select(static::TABLE);
|
||||
$query
|
||||
->fields(static::TABLE, ['target_entity_type_id', 'target_entity_id', 'target_entity_revision_id'])
|
||||
->orderBy('target_entity_revision_id', 'ASC')
|
||||
->condition('workspace', $workspace_id);
|
||||
|
||||
if ($entity_type_id) {
|
||||
$query->condition('target_entity_type_id', $entity_type_id, '=');
|
||||
|
||||
if ($entity_ids) {
|
||||
$query->condition('target_entity_id', $entity_ids, 'IN');
|
||||
}
|
||||
}
|
||||
|
||||
$tracked_revisions = [];
|
||||
foreach ($query->execute() as $record) {
|
||||
$tracked_revisions[$record->target_entity_type_id][$record->target_entity_revision_id] = $record->target_entity_id;
|
||||
}
|
||||
|
||||
return $tracked_revisions;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getAssociatedRevisions($workspace_id, $entity_type_id, $entity_ids = NULL) {
|
||||
/** @var \Drupal\Core\Entity\EntityStorageInterface $storage */
|
||||
$storage = $this->entityTypeManager->getStorage($entity_type_id);
|
||||
|
||||
// If the entity type is not using core's default entity storage, we can't
|
||||
// assume the table mapping layout so we have to return only the latest
|
||||
// tracked revisions.
|
||||
if (!$storage instanceof SqlContentEntityStorage) {
|
||||
return $this->getTrackedEntities($workspace_id, $entity_type_id, $entity_ids)[$entity_type_id];
|
||||
}
|
||||
|
||||
$entity_type = $storage->getEntityType();
|
||||
$table_mapping = $storage->getTableMapping();
|
||||
$workspace_field = $table_mapping->getColumnNames($entity_type->get('revision_metadata_keys')['workspace'])['target_id'];
|
||||
$id_field = $table_mapping->getColumnNames($entity_type->getKey('id'))['value'];
|
||||
$revision_id_field = $table_mapping->getColumnNames($entity_type->getKey('revision'))['value'];
|
||||
|
||||
$query = $this->database->select($entity_type->getRevisionTable(), 'revision');
|
||||
$query->leftJoin($entity_type->getBaseTable(), 'base', "revision.$id_field = base.$id_field");
|
||||
|
||||
$query
|
||||
->fields('revision', [$revision_id_field, $id_field])
|
||||
->condition("revision.$workspace_field", $workspace_id)
|
||||
->where("revision.$revision_id_field > base.$revision_id_field")
|
||||
->orderBy("revision.$revision_id_field", 'ASC');
|
||||
|
||||
// Restrict the result to a set of entity ID's if provided.
|
||||
if ($entity_ids) {
|
||||
$query->condition("revision.$id_field", $entity_ids, 'IN');
|
||||
}
|
||||
|
||||
return $query->execute()->fetchAllKeyed();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
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());
|
||||
|
||||
return $query->execute()->fetchCol();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function postPublish(WorkspaceInterface $workspace) {
|
||||
$this->deleteAssociations($workspace->id());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function deleteAssociations($workspace_id, $entity_type_id = NULL, $entity_ids = NULL) {
|
||||
$query = $this->database->delete(static::TABLE)
|
||||
->condition('workspace', $workspace_id);
|
||||
|
||||
if ($entity_type_id) {
|
||||
$query->condition('target_entity_type_id', $entity_type_id, '=');
|
||||
|
||||
if ($entity_ids) {
|
||||
$query->condition('target_entity_id', $entity_ids, 'IN');
|
||||
}
|
||||
}
|
||||
|
||||
$query->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function initializeWorkspace(WorkspaceInterface $workspace) {
|
||||
if ($parent_id = $workspace->parent->target_id) {
|
||||
$indexed_rows = $this->database->select(static::TABLE);
|
||||
$indexed_rows->addExpression(':new_id', 'workspace', [
|
||||
':new_id' => $workspace->id(),
|
||||
]);
|
||||
$indexed_rows->fields(static::TABLE, [
|
||||
'target_entity_type_id',
|
||||
'target_entity_id',
|
||||
'target_entity_revision_id',
|
||||
]);
|
||||
$indexed_rows->condition('workspace', $parent_id);
|
||||
$this->database->insert(static::TABLE)->from($indexed_rows)->execute();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\workspaces;
|
||||
|
||||
use Drupal\Core\Entity\RevisionableInterface;
|
||||
|
||||
/**
|
||||
* Defines an interface for the workspace_association service.
|
||||
*
|
||||
* The canonical workspace association data is stored in a revision metadata
|
||||
* field on each entity revision that is tracked by a workspace.
|
||||
*
|
||||
* For the purpose of optimizing workspace-specific queries, the default
|
||||
* implementation of this interface defines a custom 'workspace_association'
|
||||
* index table which stores only the latest revisions tracked by a workspace.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
interface WorkspaceAssociationInterface {
|
||||
|
||||
/**
|
||||
* Updates or creates the association for a given entity and a workspace.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\RevisionableInterface $entity
|
||||
* The entity to update or create from.
|
||||
* @param \Drupal\workspaces\WorkspaceInterface $workspace
|
||||
* The workspace in which the entity will be tracked.
|
||||
*/
|
||||
public function trackEntity(RevisionableInterface $entity, WorkspaceInterface $workspace);
|
||||
|
||||
/**
|
||||
* Responds to the creation of a new workspace entity.
|
||||
*
|
||||
* @param \Drupal\workspaces\WorkspaceInterface $workspace
|
||||
* The workspaces that was inserted.
|
||||
*/
|
||||
public function workspaceInsert(WorkspaceInterface $workspace);
|
||||
|
||||
/**
|
||||
* Retrieves the entities tracked by a given workspace.
|
||||
*
|
||||
* @param string $workspace_id
|
||||
* The ID of the workspace.
|
||||
* @param string|null $entity_type_id
|
||||
* (optional) An entity type ID to filter the results by. Defaults to NULL.
|
||||
* @param int[]|string[]|null $entity_ids
|
||||
* (optional) An array of entity IDs to filter the results by. Defaults to
|
||||
* NULL.
|
||||
*
|
||||
* @return array
|
||||
* Returns a multidimensional array where the first level keys are entity
|
||||
* type IDs and the values are an array of entity IDs keyed by revision IDs.
|
||||
*/
|
||||
public function getTrackedEntities($workspace_id, $entity_type_id = NULL, $entity_ids = NULL);
|
||||
|
||||
/**
|
||||
* Retrieves all content revisions tracked by a given workspace.
|
||||
*
|
||||
* Since the 'workspace_association' index table only tracks the latest
|
||||
* associated revisions, this method retrieves all the tracked revisions by
|
||||
* querying the entity type's revision table directly.
|
||||
*
|
||||
* @param string $workspace_id
|
||||
* The ID of the workspace.
|
||||
* @param string $entity_type_id
|
||||
* An entity type ID to find revisions for.
|
||||
* @param int[]|string[]|null $entity_ids
|
||||
* (optional) An array of entity IDs to filter the results by. Defaults to
|
||||
* NULL.
|
||||
*
|
||||
* @return array
|
||||
* Returns an array where the values are an array of entity IDs keyed by
|
||||
* revision IDs.
|
||||
*/
|
||||
public function getAssociatedRevisions($workspace_id, $entity_type_id, $entity_ids = NULL);
|
||||
|
||||
/**
|
||||
* Gets a list of workspace IDs in which an entity is tracked.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\RevisionableInterface $entity
|
||||
* An entity object.
|
||||
*
|
||||
* @return string[]
|
||||
* An array of workspace IDs where the given entity is tracked, or an empty
|
||||
* array if it is not tracked anywhere.
|
||||
*/
|
||||
public function getEntityTrackingWorkspaceIds(RevisionableInterface $entity);
|
||||
|
||||
/**
|
||||
* Triggers clean-up operations after publishing a workspace.
|
||||
*
|
||||
* @param \Drupal\workspaces\WorkspaceInterface $workspace
|
||||
* A workspace entity.
|
||||
*/
|
||||
public function postPublish(WorkspaceInterface $workspace);
|
||||
|
||||
/**
|
||||
* Deletes all the workspace association records for the given workspace.
|
||||
*
|
||||
* @param string $workspace_id
|
||||
* A workspace entity ID.
|
||||
* @param string|null $entity_type_id
|
||||
* (optional) The target entity type of the associations to delete. Defaults
|
||||
* to NULL.
|
||||
* @param string|null $entity_ids
|
||||
* (optional) The target entity IDs of the associations to delete. Defaults
|
||||
* to NULL.
|
||||
*/
|
||||
public function deleteAssociations($workspace_id, $entity_type_id = NULL, $entity_ids = NULL);
|
||||
|
||||
/**
|
||||
* Initializes a workspace with all the associations of its parent.
|
||||
*
|
||||
* @param \Drupal\workspaces\WorkspaceInterface $workspace
|
||||
* The workspace to be initialized.
|
||||
*/
|
||||
public function initializeWorkspace(WorkspaceInterface $workspace);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\workspaces;
|
||||
|
||||
use Drupal\Core\Cache\CacheableMetadata;
|
||||
use Drupal\Core\Cache\Context\CacheContextInterface;
|
||||
|
||||
/**
|
||||
* Defines the WorkspaceCacheContext service, for "per workspace" caching.
|
||||
*
|
||||
* Cache context ID: 'workspace'.
|
||||
*/
|
||||
class WorkspaceCacheContext implements CacheContextInterface {
|
||||
|
||||
/**
|
||||
* The workspace manager.
|
||||
*
|
||||
* @var \Drupal\workspaces\WorkspaceManagerInterface
|
||||
*/
|
||||
protected $workspaceManager;
|
||||
|
||||
/**
|
||||
* Constructs a new WorkspaceCacheContext service.
|
||||
*
|
||||
* @param \Drupal\workspaces\WorkspaceManagerInterface $workspace_manager
|
||||
* The workspace manager.
|
||||
*/
|
||||
public function __construct(WorkspaceManagerInterface $workspace_manager) {
|
||||
$this->workspaceManager = $workspace_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function getLabel() {
|
||||
return t('Workspace');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getContext() {
|
||||
return $this->workspaceManager->hasActiveWorkspace() ? $this->workspaceManager->getActiveWorkspace()->id() : 'live';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCacheableMetadata($type = NULL) {
|
||||
// The active workspace will always be stored in the user's session.
|
||||
$cacheability = new CacheableMetadata();
|
||||
$cacheability->addCacheContexts(['session']);
|
||||
|
||||
return $cacheability;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\workspaces;
|
||||
|
||||
/**
|
||||
* An exception thrown when two workspaces are in a conflicting content state.
|
||||
*/
|
||||
class WorkspaceConflictException extends \RuntimeException {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\workspaces;
|
||||
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Entity\EntityChangedInterface;
|
||||
use Drupal\user\EntityOwnerInterface;
|
||||
|
||||
/**
|
||||
* Defines an interface for the workspace entity type.
|
||||
*/
|
||||
interface WorkspaceInterface extends ContentEntityInterface, EntityChangedInterface, EntityOwnerInterface {
|
||||
|
||||
/**
|
||||
* 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';
|
||||
|
||||
/**
|
||||
* Publishes the contents of this workspace to the default (Live) workspace.
|
||||
*/
|
||||
public function publish();
|
||||
|
||||
/**
|
||||
* Determines whether the workspace is the default one or not.
|
||||
*
|
||||
* @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();
|
||||
|
||||
/**
|
||||
* Gets the workspace creation timestamp.
|
||||
*
|
||||
* @return int
|
||||
* Creation timestamp of the workspace.
|
||||
*/
|
||||
public function getCreatedTime();
|
||||
|
||||
/**
|
||||
* Sets the workspace creation timestamp.
|
||||
*
|
||||
* @param int $timestamp
|
||||
* The workspace creation timestamp.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setCreatedTime($timestamp);
|
||||
|
||||
/**
|
||||
* Determines whether the workspace has a parent.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the workspace has a parent, FALSE otherwise.
|
||||
*/
|
||||
public function hasParent();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\workspaces;
|
||||
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use Drupal\Core\Ajax\AjaxHelperTrait;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\EntityListBuilder;
|
||||
use Drupal\Core\Entity\EntityStorageInterface;
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Render\RendererInterface;
|
||||
use Drupal\Core\Url;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Defines a class to build a listing of workspace entities.
|
||||
*
|
||||
* @see \Drupal\workspaces\Entity\Workspace
|
||||
*/
|
||||
class WorkspaceListBuilder extends EntityListBuilder {
|
||||
|
||||
use AjaxHelperTrait;
|
||||
|
||||
/**
|
||||
* The workspace manager service.
|
||||
*
|
||||
* @var \Drupal\workspaces\WorkspaceManagerInterface
|
||||
*/
|
||||
protected $workspaceManager;
|
||||
|
||||
/**
|
||||
* The workspace repository service.
|
||||
*
|
||||
* @var \Drupal\workspaces\WorkspaceRepositoryInterface
|
||||
*/
|
||||
protected $workspaceRepository;
|
||||
|
||||
/**
|
||||
* The renderer service.
|
||||
*
|
||||
* @var \Drupal\Core\Render\RendererInterface
|
||||
*/
|
||||
protected $renderer;
|
||||
|
||||
/**
|
||||
* Constructs a new EntityListBuilder object.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
|
||||
* The entity type definition.
|
||||
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
|
||||
* The entity storage class.
|
||||
* @param \Drupal\workspaces\WorkspaceManagerInterface $workspace_manager
|
||||
* The workspace manager service.
|
||||
* @param \Drupal\Core\Render\RendererInterface $renderer
|
||||
* The renderer service.
|
||||
*/
|
||||
public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, WorkspaceManagerInterface $workspace_manager, WorkspaceRepositoryInterface $workspace_repository, RendererInterface $renderer) {
|
||||
parent::__construct($entity_type, $storage);
|
||||
$this->workspaceManager = $workspace_manager;
|
||||
$this->workspaceRepository = $workspace_repository;
|
||||
$this->renderer = $renderer;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
|
||||
return new static(
|
||||
$entity_type,
|
||||
$container->get('entity_type.manager')->getStorage($entity_type->id()),
|
||||
$container->get('workspaces.manager'),
|
||||
$container->get('workspaces.repository'),
|
||||
$container->get('renderer')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function load() {
|
||||
// Get all the workspace entities and sort them in tree order.
|
||||
$workspace_tree = $this->workspaceRepository->loadTree();
|
||||
$entities = array_replace($workspace_tree, $this->storage->loadMultiple());
|
||||
foreach ($entities as $id => $entity) {
|
||||
$entity->_depth = $workspace_tree[$id]['depth'];
|
||||
}
|
||||
|
||||
return $entities;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildHeader() {
|
||||
$header['label'] = $this->t('Workspace');
|
||||
$header['uid'] = $this->t('Owner');
|
||||
|
||||
return $header + parent::buildHeader();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildRow(EntityInterface $entity) {
|
||||
/** @var \Drupal\workspaces\WorkspaceInterface $entity */
|
||||
if (isset($entity->_depth) && $entity->_depth > 0) {
|
||||
$indentation = [
|
||||
'#theme' => 'indentation',
|
||||
'#size' => $entity->_depth,
|
||||
];
|
||||
}
|
||||
|
||||
$row['data'] = [
|
||||
'label' => [
|
||||
'data' => [
|
||||
'#prefix' => isset($indentation) ? $this->renderer->render($indentation) : '',
|
||||
'#markup' => $entity->label(),
|
||||
],
|
||||
],
|
||||
'owner' => $entity->getOwner()->getDisplayname(),
|
||||
];
|
||||
$row['data'] = $row['data'] + parent::buildRow($entity);
|
||||
|
||||
$active_workspace = $this->workspaceManager->getActiveWorkspace();
|
||||
if ($active_workspace && $entity->id() === $active_workspace->id()) {
|
||||
$row['class'] = 'active-workspace';
|
||||
}
|
||||
return $row;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getDefaultOperations(EntityInterface $entity) {
|
||||
/** @var \Drupal\workspaces\WorkspaceInterface $entity */
|
||||
$operations = parent::getDefaultOperations($entity);
|
||||
if (isset($operations['edit'])) {
|
||||
$operations['edit']['query']['destination'] = $entity->toUrl('collection')->toString();
|
||||
}
|
||||
|
||||
$active_workspace = $this->workspaceManager->getActiveWorkspace();
|
||||
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
|
||||
// want the 'Activate' operation to be the primary operation.
|
||||
'weight' => 0,
|
||||
'url' => $entity->toUrl('activate-form', ['query' => ['destination' => $entity->toUrl('collection')->toString()]]),
|
||||
];
|
||||
}
|
||||
|
||||
if (!$entity->hasParent()) {
|
||||
$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()]]),
|
||||
];
|
||||
}
|
||||
else {
|
||||
/** @var \Drupal\workspaces\WorkspaceInterface $parent */
|
||||
$parent = $entity->parent->entity;
|
||||
$operations['merge'] = [
|
||||
'title' => $this->t('Merge into @target_label', [
|
||||
'@target_label' => $parent->label(),
|
||||
]),
|
||||
'weight' => 5,
|
||||
'url' => Url::fromRoute('entity.workspace.merge_form',
|
||||
[
|
||||
'source_workspace' => $entity->id(),
|
||||
'target_workspace' => $parent->id(),
|
||||
],
|
||||
[
|
||||
'query' => ['destination' => $entity->toUrl('collection')->toString()],
|
||||
]
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
return $operations;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function render() {
|
||||
$build = parent::render();
|
||||
if ($this->isAjax()) {
|
||||
$this->offCanvasRender($build);
|
||||
}
|
||||
else {
|
||||
$build['#attached'] = [
|
||||
'library' => ['workspaces/drupal.workspaces.overview'],
|
||||
];
|
||||
}
|
||||
return $build;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the off canvas elements.
|
||||
*
|
||||
* @param array $build
|
||||
* A render array.
|
||||
*/
|
||||
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',
|
||||
];
|
||||
}
|
||||
|
||||
$build['active_workspace'] = [
|
||||
'#type' => 'container',
|
||||
'#weight' => -20,
|
||||
'#attributes' => [
|
||||
'class' => array_merge(['active-workspace'], $active_workspace_classes),
|
||||
],
|
||||
'title' => [
|
||||
'#type' => 'html_tag',
|
||||
'#tag' => 'div',
|
||||
'#value' => $this->t('Current workspace:'),
|
||||
'#attributes' => ['class' => 'active-workspace__title'],
|
||||
],
|
||||
'label' => [
|
||||
'#type' => 'container',
|
||||
'#attributes' => ['class' => 'active-workspace__label'],
|
||||
'value' => [
|
||||
'#type' => 'html_tag',
|
||||
'#tag' => 'span',
|
||||
'#value' => $active_workspace ? $active_workspace->label() : $this->t('Live'),
|
||||
],
|
||||
],
|
||||
];
|
||||
if ($active_workspace) {
|
||||
$build['active_workspace']['label']['manage'] = [
|
||||
'#type' => 'link',
|
||||
'#title' => $this->t('Manage workspace'),
|
||||
'#url' => $active_workspace->toUrl('edit-form'),
|
||||
'#attributes' => [
|
||||
'class' => ['active-workspace__manage'],
|
||||
],
|
||||
];
|
||||
$build['active_workspace']['actions'] = [
|
||||
'#type' => 'container',
|
||||
'#weight' => 20,
|
||||
'#attributes' => [
|
||||
'class' => ['active-workspace__actions'],
|
||||
],
|
||||
];
|
||||
if (!$active_workspace->hasParent()) {
|
||||
$build['active_workspace']['actions']['deploy'] = [
|
||||
'#type' => 'link',
|
||||
'#title' => $this->t('Deploy content'),
|
||||
'#url' => $active_workspace->toUrl('deploy-form', ['query' => ['destination' => $active_workspace->toUrl('collection')->toString()]]),
|
||||
'#attributes' => [
|
||||
'class' => ['button', 'active-workspace__button'],
|
||||
],
|
||||
];
|
||||
}
|
||||
else {
|
||||
$build['active_workspace']['actions']['merge'] = [
|
||||
'#type' => 'link',
|
||||
'#title' => $this->t('Merge content'),
|
||||
'#url' => Url::fromRoute('entity.workspace.merge_form',
|
||||
[
|
||||
'source_workspace' => $active_workspace->id(),
|
||||
'target_workspace' => $active_workspace->parent->target_id,
|
||||
],
|
||||
[
|
||||
'query' => ['destination' => $active_workspace->toUrl('collection')->toString()],
|
||||
]
|
||||
),
|
||||
'#attributes' => [
|
||||
'class' => ['button', 'active-workspace__button'],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$items = [];
|
||||
$rows = array_slice($build['table']['#rows'], 0, 5, TRUE);
|
||||
foreach ($rows as $id => $row) {
|
||||
if (!$active_workspace || $active_workspace->id() !== $id) {
|
||||
$url = Url::fromRoute('entity.workspace.activate_form', ['workspace' => $id], ['query' => $this->getDestinationArray()]);
|
||||
$items[] = [
|
||||
'#type' => 'link',
|
||||
'#title' => ltrim($row['data']['label']['data']['#markup']),
|
||||
'#url' => $url,
|
||||
'#attributes' => [
|
||||
'class' => ['use-ajax', 'workspaces__item', 'workspaces__item--not-default'],
|
||||
'data-dialog-type' => 'modal',
|
||||
'data-dialog-options' => Json::encode([
|
||||
'width' => 500,
|
||||
]),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 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_list'] = [
|
||||
'#type' => 'container',
|
||||
'#attributes' => [
|
||||
'class' => 'workspaces',
|
||||
],
|
||||
];
|
||||
$build['workspaces_list']['workspaces'] = [
|
||||
'#theme' => 'item_list',
|
||||
'#title' => $this->t('Other workspaces:'),
|
||||
'#items' => $items,
|
||||
'#wrapper_attributes' => ['class' => ['workspaces__list']],
|
||||
'#cache' => [
|
||||
'contexts' => $this->entityType->getListCacheContexts(),
|
||||
'tags' => $this->entityType->getListCacheTags(),
|
||||
],
|
||||
];
|
||||
$build['workspaces_list']['all_workspaces'] = [
|
||||
'#type' => 'link',
|
||||
'#title' => $this->t('View all workspaces'),
|
||||
'#url' => Url::fromRoute('entity.workspace.collection'),
|
||||
'#attributes' => [
|
||||
'class' => ['all-workspaces'],
|
||||
],
|
||||
];
|
||||
unset($build['table']);
|
||||
unset($build['pager']);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\workspaces;
|
||||
|
||||
use Drupal\Core\Cache\MemoryCache\MemoryCacheInterface;
|
||||
use Drupal\Core\DependencyInjection\ClassResolverInterface;
|
||||
use Drupal\Core\Entity\EntityPublishedInterface;
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Session\AccountProxyInterface;
|
||||
use Drupal\Core\Site\Settings;
|
||||
use Drupal\Core\State\StateInterface;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
||||
/**
|
||||
* Provides the workspace manager.
|
||||
*/
|
||||
class WorkspaceManager implements WorkspaceManagerInterface {
|
||||
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* An array of entity type IDs that can not belong to a workspace.
|
||||
*
|
||||
* By default, only entity types which are revisionable and publishable can
|
||||
* belong to a workspace.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $blacklist = [
|
||||
'workspace_association' => 'workspace_association',
|
||||
'workspace' => 'workspace',
|
||||
];
|
||||
|
||||
/**
|
||||
* The request stack.
|
||||
*
|
||||
* @var \Symfony\Component\HttpFoundation\RequestStack
|
||||
*/
|
||||
protected $requestStack;
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The entity memory cache service.
|
||||
*
|
||||
* @var \Drupal\Core\Cache\MemoryCache\MemoryCacheInterface
|
||||
*/
|
||||
protected $entityMemoryCache;
|
||||
|
||||
/**
|
||||
* The current user.
|
||||
*
|
||||
* @var \Drupal\Core\Session\AccountProxyInterface
|
||||
*/
|
||||
protected $currentUser;
|
||||
|
||||
/**
|
||||
* The state service.
|
||||
*
|
||||
* @var \Drupal\Core\State\StateInterface
|
||||
*/
|
||||
protected $state;
|
||||
|
||||
/**
|
||||
* A logger instance.
|
||||
*
|
||||
* @var \Psr\Log\LoggerInterface
|
||||
*/
|
||||
protected $logger;
|
||||
|
||||
/**
|
||||
* The class resolver.
|
||||
*
|
||||
* @var \Drupal\Core\DependencyInjection\ClassResolverInterface
|
||||
*/
|
||||
protected $classResolver;
|
||||
|
||||
/**
|
||||
* The workspace association service.
|
||||
*
|
||||
* @var \Drupal\workspaces\WorkspaceAssociationInterface
|
||||
*/
|
||||
protected $workspaceAssociation;
|
||||
|
||||
/**
|
||||
* The workspace negotiator service IDs.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $negotiatorIds;
|
||||
|
||||
/**
|
||||
* The current active workspace or FALSE if there is no active workspace.
|
||||
*
|
||||
* @var \Drupal\workspaces\WorkspaceInterface|false
|
||||
*/
|
||||
protected $activeWorkspace;
|
||||
|
||||
/**
|
||||
* Constructs a new WorkspaceManager.
|
||||
*
|
||||
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
|
||||
* The request stack.
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager.
|
||||
* @param \Drupal\Core\Cache\MemoryCache\MemoryCacheInterface $entity_memory_cache
|
||||
* The entity memory cache service.
|
||||
* @param \Drupal\Core\Session\AccountProxyInterface $current_user
|
||||
* The current user.
|
||||
* @param \Drupal\Core\State\StateInterface $state
|
||||
* The state service.
|
||||
* @param \Psr\Log\LoggerInterface $logger
|
||||
* A logger instance.
|
||||
* @param \Drupal\Core\DependencyInjection\ClassResolverInterface $class_resolver
|
||||
* The class resolver.
|
||||
* @param \Drupal\workspaces\WorkspaceAssociationInterface $workspace_association
|
||||
* The workspace association service.
|
||||
* @param array $negotiator_ids
|
||||
* The workspace negotiator service IDs.
|
||||
*/
|
||||
public function __construct(RequestStack $request_stack, EntityTypeManagerInterface $entity_type_manager, MemoryCacheInterface $entity_memory_cache, AccountProxyInterface $current_user, StateInterface $state, LoggerInterface $logger, ClassResolverInterface $class_resolver, WorkspaceAssociationInterface $workspace_association, array $negotiator_ids) {
|
||||
$this->requestStack = $request_stack;
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->entityMemoryCache = $entity_memory_cache;
|
||||
$this->currentUser = $current_user;
|
||||
$this->state = $state;
|
||||
$this->logger = $logger;
|
||||
$this->classResolver = $class_resolver;
|
||||
$this->workspaceAssociation = $workspace_association;
|
||||
$this->negotiatorIds = $negotiator_ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isEntityTypeSupported(EntityTypeInterface $entity_type) {
|
||||
// First, check if we already determined whether this entity type is
|
||||
// supported or not.
|
||||
if (isset($this->blacklist[$entity_type->id()])) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if ($entity_type->entityClassImplements(EntityPublishedInterface::class) && $entity_type->isRevisionable()) {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// This entity type can not belong to a workspace, add it to the blacklist.
|
||||
$this->blacklist[$entity_type->id()] = $entity_type->id();
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getSupportedEntityTypes() {
|
||||
$entity_types = [];
|
||||
foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
|
||||
if ($this->isEntityTypeSupported($entity_type)) {
|
||||
$entity_types[$entity_type_id] = $entity_type;
|
||||
}
|
||||
}
|
||||
return $entity_types;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function hasActiveWorkspace() {
|
||||
return $this->getActiveWorkspace() !== FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getActiveWorkspace() {
|
||||
if (!isset($this->activeWorkspace)) {
|
||||
$request = $this->requestStack->getCurrentRequest();
|
||||
foreach ($this->negotiatorIds as $negotiator_id) {
|
||||
$negotiator = $this->classResolver->getInstanceFromDefinition($negotiator_id);
|
||||
if ($negotiator->applies($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;
|
||||
}
|
||||
|
||||
return $this->activeWorkspace;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setActiveWorkspace(WorkspaceInterface $workspace) {
|
||||
$this->doSwitchWorkspace($workspace);
|
||||
|
||||
// Set the workspace on the proper negotiator.
|
||||
$request = $this->requestStack->getCurrentRequest();
|
||||
foreach ($this->negotiatorIds as $negotiator_id) {
|
||||
$negotiator = $this->classResolver->getInstanceFromDefinition($negotiator_id);
|
||||
if ($negotiator->applies($request)) {
|
||||
$negotiator->setActiveWorkspace($workspace);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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|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($workspace) {
|
||||
// If the current user doesn't have access to view the workspace, they
|
||||
// shouldn't be allowed to switch to it.
|
||||
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(),
|
||||
]);
|
||||
throw new WorkspaceAccessException('The user does not have permission to view that 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) {
|
||||
return 'entity.memory_cache:' . $entity_type_id;
|
||||
}, array_keys($this->getSupportedEntityTypes()));
|
||||
$this->entityMemoryCache->invalidateTags($cache_tags_to_invalidate);
|
||||
|
||||
// Clear the static cache for path aliases. We can't inject the path alias
|
||||
// manager service because it would create a circular dependency.
|
||||
\Drupal::service('path_alias.manager')->cacheClear();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function executeInWorkspace($workspace_id, callable $function) {
|
||||
/** @var \Drupal\workspaces\WorkspaceInterface $workspace */
|
||||
$workspace = $this->entityTypeManager->getStorage('workspace')->load($workspace_id);
|
||||
|
||||
if (!$workspace) {
|
||||
throw new \InvalidArgumentException('The ' . $workspace_id . ' workspace does not exist.');
|
||||
}
|
||||
|
||||
$previous_active_workspace = $this->getActiveWorkspace();
|
||||
$this->doSwitchWorkspace($workspace);
|
||||
$result = $function();
|
||||
$this->doSwitchWorkspace($previous_active_workspace);
|
||||
|
||||
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->hasActiveWorkspace();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function purgeDeletedWorkspacesBatch() {
|
||||
$deleted_workspace_ids = $this->state->get('workspace.deleted', []);
|
||||
|
||||
// Bail out early if there are no workspaces to purge.
|
||||
if (empty($deleted_workspace_ids)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$batch_size = Settings::get('entity_update_batch_size', 50);
|
||||
|
||||
// Get the first deleted workspace from the list and delete the revisions
|
||||
// associated with it, along with the workspace association records.
|
||||
$workspace_id = reset($deleted_workspace_ids);
|
||||
$tracked_entities = $this->workspaceAssociation->getTrackedEntities($workspace_id);
|
||||
|
||||
$count = 1;
|
||||
foreach ($tracked_entities as $entity_type_id => $entities) {
|
||||
$associated_entity_storage = $this->entityTypeManager->getStorage($entity_type_id);
|
||||
$associated_revisions = $this->workspaceAssociation->getAssociatedRevisions($workspace_id, $entity_type_id);
|
||||
foreach (array_keys($associated_revisions) as $revision_id) {
|
||||
if ($count > $batch_size) {
|
||||
continue 2;
|
||||
}
|
||||
|
||||
// Delete the associated entity revision.
|
||||
$associated_entity_storage->deleteRevision($revision_id);
|
||||
$count++;
|
||||
}
|
||||
// Delete the workspace association entries.
|
||||
$this->workspaceAssociation->deleteAssociations($workspace_id, $entity_type_id, $entities);
|
||||
}
|
||||
|
||||
// The purging operation above might have taken a long time, so we need to
|
||||
// request a fresh list of tracked entities. If it is empty, we can go ahead
|
||||
// and remove the deleted workspace ID entry from state.
|
||||
if (!$this->workspaceAssociation->getTrackedEntities($workspace_id)) {
|
||||
unset($deleted_workspace_ids[$workspace_id]);
|
||||
$this->state->set('workspace.deleted', $deleted_workspace_ids);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\workspaces;
|
||||
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
|
||||
/**
|
||||
* Provides an interface for managing Workspaces.
|
||||
*/
|
||||
interface WorkspaceManagerInterface {
|
||||
|
||||
/**
|
||||
* Returns whether an entity type can belong to a workspace or not.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
|
||||
* The entity type to check.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the entity type can belong to a workspace, FALSE otherwise.
|
||||
*/
|
||||
public function isEntityTypeSupported(EntityTypeInterface $entity_type);
|
||||
|
||||
/**
|
||||
* Returns an array of entity types that can belong to workspaces.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\EntityTypeInterface[]
|
||||
* The entity types what can belong to workspaces.
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @return \Drupal\workspaces\WorkspaceInterface
|
||||
* The active workspace entity object.
|
||||
*/
|
||||
public function getActiveWorkspace();
|
||||
|
||||
/**
|
||||
* Sets the active workspace via the workspace negotiators.
|
||||
*
|
||||
* @param \Drupal\workspaces\WorkspaceInterface $workspace
|
||||
* The workspace to set as active.
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @throws \Drupal\workspaces\WorkspaceAccessException
|
||||
* Thrown when the current user doesn't have access to view the workspace.
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @param string $workspace_id
|
||||
* The ID of a workspace.
|
||||
* @param callable $function
|
||||
* The callback to be executed.
|
||||
*
|
||||
* @return mixed
|
||||
* The callable's return value.
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
|
||||
* The entity type to check.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the entity operations or queries should be altered in the current
|
||||
* request, FALSE otherwise.
|
||||
*/
|
||||
public function shouldAlterOperations(EntityTypeInterface $entity_type);
|
||||
|
||||
/**
|
||||
* Deletes the revisions associated with deleted workspaces.
|
||||
*/
|
||||
public function purgeDeletedWorkspacesBatch();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\workspaces;
|
||||
|
||||
use Drupal\Core\Cache\CacheTagsInvalidatorInterface;
|
||||
use Drupal\Core\Database\Connection;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
|
||||
/**
|
||||
* Default implementation of the workspace merger.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class WorkspaceMerger implements WorkspaceMergerInterface {
|
||||
|
||||
/**
|
||||
* The source workspace entity.
|
||||
*
|
||||
* @var \Drupal\workspaces\WorkspaceInterface
|
||||
*/
|
||||
protected $sourceWorkspace;
|
||||
|
||||
/**
|
||||
* The target workspace entity.
|
||||
*
|
||||
* @var \Drupal\workspaces\WorkspaceInterface
|
||||
*/
|
||||
protected $targetWorkspace;
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The database connection.
|
||||
*
|
||||
* @var \Drupal\Core\Database\Connection
|
||||
*/
|
||||
protected $database;
|
||||
|
||||
/**
|
||||
* The workspace association service.
|
||||
*
|
||||
* @var \Drupal\workspaces\WorkspaceAssociationInterface
|
||||
*/
|
||||
protected $workspaceAssociation;
|
||||
|
||||
/**
|
||||
* The cache tag invalidator.
|
||||
*
|
||||
* @var \Drupal\Core\Cache\CacheTagsInvalidatorInterface
|
||||
*/
|
||||
protected $cacheTagsInvalidator;
|
||||
|
||||
/**
|
||||
* Constructs a new WorkspaceMerger.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager.
|
||||
* @param \Drupal\Core\Database\Connection $database
|
||||
* Database connection.
|
||||
* @param \Drupal\workspaces\WorkspaceAssociationInterface $workspace_association
|
||||
* The workspace association service.
|
||||
* @param \Drupal\Core\Cache\CacheTagsInvalidatorInterface $cache_tags_invalidator
|
||||
* The cache tags invalidator service.
|
||||
* @param \Drupal\workspaces\WorkspaceInterface $source
|
||||
* The source workspace.
|
||||
* @param \Drupal\workspaces\WorkspaceInterface $target
|
||||
* The target workspace.
|
||||
*/
|
||||
public function __construct(EntityTypeManagerInterface $entity_type_manager, Connection $database, WorkspaceAssociationInterface $workspace_association, CacheTagsInvalidatorInterface $cache_tags_invalidator, WorkspaceInterface $source, WorkspaceInterface $target) {
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->database = $database;
|
||||
$this->workspaceAssociation = $workspace_association;
|
||||
$this->cacheTagsInvalidator = $cache_tags_invalidator;
|
||||
$this->sourceWorkspace = $source;
|
||||
$this->targetWorkspace = $target;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function merge() {
|
||||
if (!$this->sourceWorkspace->hasParent() || $this->sourceWorkspace->parent->target_id != $this->targetWorkspace->id()) {
|
||||
throw new \InvalidArgumentException('The contents of a workspace can only be merged into its parent workspace.');
|
||||
}
|
||||
|
||||
if ($this->checkConflictsOnTarget()) {
|
||||
throw new WorkspaceConflictException();
|
||||
}
|
||||
|
||||
$transaction = $this->database->startTransaction();
|
||||
try {
|
||||
foreach ($this->getDifferringRevisionIdsOnSource() as $entity_type_id => $revision_difference) {
|
||||
$revisions_on_source = $this->entityTypeManager->getStorage($entity_type_id)
|
||||
->loadMultipleRevisions(array_keys($revision_difference));
|
||||
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
|
||||
foreach ($revisions_on_source as $revision) {
|
||||
// Track all the differing revisions from the source workspace in
|
||||
// the context of the target workspace. This will automatically
|
||||
// update all the descendants of the target workspace as well.
|
||||
$this->workspaceAssociation->trackEntity($revision, $this->targetWorkspace);
|
||||
}
|
||||
|
||||
// Since we're not saving entity objects, we need to invalidate the list
|
||||
// cache tags manually.
|
||||
$entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
|
||||
$this->cacheTagsInvalidator->invalidateTags($entity_type->getListCacheTags());
|
||||
}
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
$transaction->rollBack();
|
||||
watchdog_exception('workspaces', $e);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getSourceLabel() {
|
||||
return $this->sourceWorkspace->label();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getTargetLabel() {
|
||||
return $this->targetWorkspace->label();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function checkConflictsOnTarget() {
|
||||
// Nothing to do for now, we can not get to a conflicting state because an
|
||||
// entity which is being edited in a workspace can not be edited in any
|
||||
// other workspace.
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getDifferringRevisionIdsOnTarget() {
|
||||
$target_revision_difference = [];
|
||||
|
||||
$tracked_entities_on_source = $this->workspaceAssociation->getTrackedEntities($this->sourceWorkspace->id());
|
||||
$tracked_entities_on_target = $this->workspaceAssociation->getTrackedEntities($this->targetWorkspace->id());
|
||||
foreach ($tracked_entities_on_target as $entity_type_id => $tracked_revisions) {
|
||||
// Now we compare the revision IDs which are tracked by the target
|
||||
// workspace to those that are tracked by the source workspace, and the
|
||||
// difference between these two arrays gives us all the entities which
|
||||
// have a different revision ID on the target.
|
||||
if (!isset($tracked_entities_on_source[$entity_type_id])) {
|
||||
$target_revision_difference[$entity_type_id] = $tracked_revisions;
|
||||
}
|
||||
elseif ($revision_difference = array_diff_key($tracked_revisions, $tracked_entities_on_source[$entity_type_id])) {
|
||||
$target_revision_difference[$entity_type_id] = $revision_difference;
|
||||
}
|
||||
}
|
||||
|
||||
return $target_revision_difference;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getDifferringRevisionIdsOnSource() {
|
||||
$source_revision_difference = [];
|
||||
|
||||
$tracked_entities_on_source = $this->workspaceAssociation->getTrackedEntities($this->sourceWorkspace->id());
|
||||
$tracked_entities_on_target = $this->workspaceAssociation->getTrackedEntities($this->targetWorkspace->id());
|
||||
foreach ($tracked_entities_on_source as $entity_type_id => $tracked_revisions) {
|
||||
// Now we compare the revision IDs which are tracked by the source
|
||||
// workspace to those that are tracked by the target workspace, and the
|
||||
// difference between these two arrays gives us all the entities which
|
||||
// have a different revision ID on the source.
|
||||
if (!isset($tracked_entities_on_target[$entity_type_id])) {
|
||||
$source_revision_difference[$entity_type_id] = $tracked_revisions;
|
||||
}
|
||||
elseif ($revision_difference = array_diff_key($tracked_revisions, $tracked_entities_on_target[$entity_type_id])) {
|
||||
$source_revision_difference[$entity_type_id] = $revision_difference;
|
||||
}
|
||||
}
|
||||
|
||||
return $source_revision_difference;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getNumberOfChangesOnTarget() {
|
||||
$total_changes = $this->getDifferringRevisionIdsOnTarget();
|
||||
return count($total_changes, COUNT_RECURSIVE) - count($total_changes);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getNumberOfChangesOnSource() {
|
||||
$total_changes = $this->getDifferringRevisionIdsOnSource();
|
||||
return count($total_changes, COUNT_RECURSIVE) - count($total_changes);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\workspaces;
|
||||
|
||||
/**
|
||||
* Defines an interface for the workspace merger.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
interface WorkspaceMergerInterface extends WorkspaceOperationInterface {
|
||||
|
||||
/**
|
||||
* Merges the contents of the source workspace into the target workspace.
|
||||
*/
|
||||
public function merge();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\workspaces;
|
||||
|
||||
use Drupal\Core\Cache\CacheTagsInvalidatorInterface;
|
||||
use Drupal\Core\Database\Connection;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
|
||||
/**
|
||||
* Defines a factory class for workspace operations.
|
||||
*
|
||||
* @see \Drupal\workspaces\WorkspaceOperationInterface
|
||||
* @see \Drupal\workspaces\WorkspacePublisherInterface
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class WorkspaceOperationFactory {
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The database connection.
|
||||
*
|
||||
* @var \Drupal\Core\Database\Connection
|
||||
*/
|
||||
protected $database;
|
||||
|
||||
/**
|
||||
* The workspace manager.
|
||||
*
|
||||
* @var \Drupal\workspaces\WorkspaceManagerInterface
|
||||
*/
|
||||
protected $workspaceManager;
|
||||
|
||||
/**
|
||||
* The workspace association service.
|
||||
*
|
||||
* @var \Drupal\workspaces\WorkspaceAssociationInterface
|
||||
*/
|
||||
protected $workspaceAssociation;
|
||||
|
||||
/**
|
||||
* The cache tags invalidator.
|
||||
*
|
||||
* @var \Drupal\Core\Cache\CacheTagsInvalidatorInterface
|
||||
*/
|
||||
protected $cacheTagsInvalidator;
|
||||
|
||||
/**
|
||||
* Constructs a new WorkspacePublisher.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager.
|
||||
* @param \Drupal\Core\Database\Connection $database
|
||||
* Database connection.
|
||||
* @param \Drupal\workspaces\WorkspaceManagerInterface $workspace_manager
|
||||
* The workspace manager service.
|
||||
* @param \Drupal\workspaces\WorkspaceAssociationInterface $workspace_association
|
||||
* The workspace association service.
|
||||
* @param \Drupal\Core\Cache\CacheTagsInvalidatorInterface $cache_tags_invalidator
|
||||
* The cache tags invalidator service.
|
||||
*/
|
||||
public function __construct(EntityTypeManagerInterface $entity_type_manager, Connection $database, WorkspaceManagerInterface $workspace_manager, WorkspaceAssociationInterface $workspace_association, CacheTagsInvalidatorInterface $cache_tags_invalidator) {
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->database = $database;
|
||||
$this->workspaceManager = $workspace_manager;
|
||||
$this->workspaceAssociation = $workspace_association;
|
||||
$this->cacheTagsInvalidator = $cache_tags_invalidator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the workspace publisher.
|
||||
*
|
||||
* @param \Drupal\workspaces\WorkspaceInterface $source
|
||||
* A workspace entity.
|
||||
*
|
||||
* @return \Drupal\workspaces\WorkspacePublisherInterface
|
||||
* A workspace publisher object.
|
||||
*/
|
||||
public function getPublisher(WorkspaceInterface $source) {
|
||||
return new WorkspacePublisher($this->entityTypeManager, $this->database, $this->workspaceManager, $this->workspaceAssociation, $source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the workspace merger.
|
||||
*
|
||||
* @param \Drupal\workspaces\WorkspaceInterface $source
|
||||
* The source workspace entity.
|
||||
* @param \Drupal\workspaces\WorkspaceInterface $target
|
||||
* The target workspace entity.
|
||||
*
|
||||
* @return \Drupal\workspaces\WorkspaceMergerInterface
|
||||
* A workspace merger object.
|
||||
*/
|
||||
public function getMerger(WorkspaceInterface $source, WorkspaceInterface $target) {
|
||||
return new WorkspaceMerger($this->entityTypeManager, $this->database, $this->workspaceAssociation, $this->cacheTagsInvalidator, $source, $target);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\workspaces;
|
||||
|
||||
/**
|
||||
* Defines an interface for workspace operations.
|
||||
*
|
||||
* Example operations are publishing, merging and syncing with a remote
|
||||
* workspace.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
interface WorkspaceOperationInterface {
|
||||
|
||||
/**
|
||||
* Returns the human-readable label of the source.
|
||||
*
|
||||
* @return string
|
||||
* The source label.
|
||||
*/
|
||||
public function getSourceLabel();
|
||||
|
||||
/**
|
||||
* Returns the human-readable label of the target.
|
||||
*
|
||||
* @return string
|
||||
* The target label.
|
||||
*/
|
||||
public function getTargetLabel();
|
||||
|
||||
/**
|
||||
* Checks if there are any conflicts between the source and the target.
|
||||
*
|
||||
* @return array
|
||||
* Returns an array consisting of the number of conflicts between the source
|
||||
* and the target, keyed by the conflict type constant.
|
||||
*/
|
||||
public function checkConflictsOnTarget();
|
||||
|
||||
/**
|
||||
* Gets the revision identifiers for items which have changed on the target.
|
||||
*
|
||||
* @return array
|
||||
* A multidimensional array of revision identifiers, keyed by entity type
|
||||
* IDs.
|
||||
*/
|
||||
public function getDifferringRevisionIdsOnTarget();
|
||||
|
||||
/**
|
||||
* Gets the revision identifiers for items which have changed on the source.
|
||||
*
|
||||
* @return array
|
||||
* A multidimensional array of revision identifiers, keyed by entity type
|
||||
* IDs.
|
||||
*/
|
||||
public function getDifferringRevisionIdsOnSource();
|
||||
|
||||
/**
|
||||
* Gets the total number of items which have changed on the target.
|
||||
*
|
||||
* This returns the aggregated changes count across all entity types.
|
||||
* For example, if two nodes and one taxonomy term have changed on the target,
|
||||
* the return value is 3.
|
||||
*
|
||||
* @return int
|
||||
* The number of differing revisions.
|
||||
*/
|
||||
public function getNumberOfChangesOnTarget();
|
||||
|
||||
/**
|
||||
* Gets the total number of items which have changed on the source.
|
||||
*
|
||||
* This returns the aggregated changes count across all entity types.
|
||||
* For example, if two nodes and one taxonomy term have changed on the source,
|
||||
* the return value is 3.
|
||||
*
|
||||
* @return int
|
||||
* The number of differing revisions.
|
||||
*/
|
||||
public function getNumberOfChangesOnSource();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\workspaces;
|
||||
|
||||
use Drupal\Core\Access\AccessResultReasonInterface;
|
||||
use Drupal\Core\Database\Connection;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* Default implementation of the workspace publisher.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class WorkspacePublisher implements WorkspacePublisherInterface {
|
||||
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* The source workspace entity.
|
||||
*
|
||||
* @var \Drupal\workspaces\WorkspaceInterface
|
||||
*/
|
||||
protected $sourceWorkspace;
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The database connection.
|
||||
*
|
||||
* @var \Drupal\Core\Database\Connection
|
||||
*/
|
||||
protected $database;
|
||||
|
||||
/**
|
||||
* The workspace manager.
|
||||
*
|
||||
* @var \Drupal\workspaces\WorkspaceManagerInterface
|
||||
*/
|
||||
protected $workspaceManager;
|
||||
|
||||
/**
|
||||
* The workspace association service.
|
||||
*
|
||||
* @var \Drupal\workspaces\WorkspaceAssociationInterface
|
||||
*/
|
||||
protected $workspaceAssociation;
|
||||
|
||||
/**
|
||||
* Constructs a new WorkspacePublisher.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager.
|
||||
* @param \Drupal\Core\Database\Connection $database
|
||||
* Database connection.
|
||||
* @param \Drupal\workspaces\WorkspaceManagerInterface $workspace_manager
|
||||
* The workspace manager.
|
||||
* @param \Drupal\workspaces\WorkspaceAssociationInterface $workspace_association
|
||||
* The workspace association service.
|
||||
*/
|
||||
public function __construct(EntityTypeManagerInterface $entity_type_manager, Connection $database, WorkspaceManagerInterface $workspace_manager, WorkspaceAssociationInterface $workspace_association, WorkspaceInterface $source) {
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->database = $database;
|
||||
$this->workspaceManager = $workspace_manager;
|
||||
$this->workspaceAssociation = $workspace_association;
|
||||
$this->sourceWorkspace = $source;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function publish() {
|
||||
$publish_access = $this->sourceWorkspace->access('publish', NULL, TRUE);
|
||||
if (!$publish_access->isAllowed()) {
|
||||
$message = $publish_access instanceof AccessResultReasonInterface ? $publish_access->getReason() : '';
|
||||
throw new WorkspaceAccessException($message);
|
||||
}
|
||||
|
||||
if ($this->checkConflictsOnTarget()) {
|
||||
throw new WorkspaceConflictException();
|
||||
}
|
||||
|
||||
$transaction = $this->database->startTransaction();
|
||||
try {
|
||||
// @todo Handle the publishing of a workspace with a batch operation in
|
||||
// https://www.drupal.org/node/2958752.
|
||||
$this->workspaceManager->executeOutsideWorkspace(function () {
|
||||
foreach ($this->getDifferringRevisionIdsOnSource() as $entity_type_id => $revision_difference) {
|
||||
|
||||
$entity_revisions = $this->entityTypeManager->getStorage($entity_type_id)
|
||||
->loadMultipleRevisions(array_keys($revision_difference));
|
||||
$default_revisions = $this->entityTypeManager->getStorage($entity_type_id)
|
||||
->loadMultiple(array_values($revision_difference));
|
||||
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
|
||||
foreach ($entity_revisions as $entity) {
|
||||
// When pushing workspace-specific revisions to the default
|
||||
// workspace (Live), we simply need to mark them as default
|
||||
// revisions.
|
||||
$entity->setSyncing(TRUE);
|
||||
$entity->isDefaultRevision(TRUE);
|
||||
|
||||
// The default revision is not workspace-specific anymore.
|
||||
$field_name = $entity->getEntityType()->getRevisionMetadataKey('workspace');
|
||||
$entity->{$field_name}->target_id = NULL;
|
||||
|
||||
$entity->original = $default_revisions[$entity->id()];
|
||||
$entity->save();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
$transaction->rollBack();
|
||||
watchdog_exception('workspaces', $e);
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// Notify the workspace association that a workspace has been published.
|
||||
$this->workspaceAssociation->postPublish($this->sourceWorkspace);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getSourceLabel() {
|
||||
return $this->sourceWorkspace->label();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getTargetLabel() {
|
||||
return $this->t('Live');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function checkConflictsOnTarget() {
|
||||
// Nothing to do for now, we can not get to a conflicting state because an
|
||||
// entity which is being edited in a workspace can not be edited in any
|
||||
// other workspace.
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getDifferringRevisionIdsOnTarget() {
|
||||
$target_revision_difference = [];
|
||||
|
||||
$tracked_entities = $this->workspaceAssociation->getTrackedEntities($this->sourceWorkspace->id());
|
||||
foreach ($tracked_entities as $entity_type_id => $tracked_revisions) {
|
||||
$entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
|
||||
|
||||
// Get the latest revision IDs for all the entities that are tracked by
|
||||
// the source workspace.
|
||||
$query = $this->entityTypeManager
|
||||
->getStorage($entity_type_id)
|
||||
->getQuery()
|
||||
->condition($entity_type->getKey('id'), $tracked_revisions, 'IN')
|
||||
->latestRevision();
|
||||
$result = $query->execute();
|
||||
|
||||
// Now we compare the revision IDs which are tracked by the source
|
||||
// workspace to the latest revision IDs of those entities and the
|
||||
// difference between these two arrays gives us all the entities which
|
||||
// have been modified on the target.
|
||||
if ($revision_difference = array_diff_key($result, $tracked_revisions)) {
|
||||
$target_revision_difference[$entity_type_id] = $revision_difference;
|
||||
}
|
||||
}
|
||||
|
||||
return $target_revision_difference;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getDifferringRevisionIdsOnSource() {
|
||||
// Get the Workspace association revisions which haven't been pushed yet.
|
||||
return $this->workspaceAssociation->getTrackedEntities($this->sourceWorkspace->id());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getNumberOfChangesOnTarget() {
|
||||
$total_changes = $this->getDifferringRevisionIdsOnTarget();
|
||||
return count($total_changes, COUNT_RECURSIVE) - count($total_changes);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getNumberOfChangesOnSource() {
|
||||
$total_changes = $this->getDifferringRevisionIdsOnSource();
|
||||
return count($total_changes, COUNT_RECURSIVE) - count($total_changes);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\workspaces;
|
||||
|
||||
/**
|
||||
* Defines an interface for the workspace publisher.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
interface WorkspacePublisherInterface extends WorkspaceOperationInterface {
|
||||
|
||||
/**
|
||||
* Publishes the contents of a workspace to the default (Live) workspace.
|
||||
*/
|
||||
public function publish();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\workspaces;
|
||||
|
||||
use Drupal\Component\Graph\Graph;
|
||||
use Drupal\Core\Cache\Cache;
|
||||
use Drupal\Core\Cache\CacheBackendInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
|
||||
/**
|
||||
* Provides the default workspace tree lookup operations.
|
||||
*/
|
||||
class WorkspaceRepository implements WorkspaceRepositoryInterface {
|
||||
|
||||
/**
|
||||
* The entity type manager service.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The cache backend used to store the workspace tree.
|
||||
*
|
||||
* @var \Drupal\Core\Cache\CacheBackendInterface
|
||||
*/
|
||||
protected $cache;
|
||||
|
||||
/**
|
||||
* An array of tree items, keyed by workspace IDs and sorted in tree order.
|
||||
*
|
||||
* @var array|null
|
||||
*/
|
||||
protected $tree;
|
||||
|
||||
/**
|
||||
* Constructs a new WorkspaceRepository instance.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager service.
|
||||
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
|
||||
* The cache backend.
|
||||
*/
|
||||
public function __construct(EntityTypeManagerInterface $entity_type_manager, CacheBackendInterface $cache_backend) {
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->cache = $cache_backend;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function loadTree() {
|
||||
if (!isset($this->tree)) {
|
||||
$cache = $this->cache->get('workspace_tree');
|
||||
if ($cache) {
|
||||
$this->tree = $cache->data;
|
||||
return $this->tree;
|
||||
}
|
||||
|
||||
/** @var \Drupal\workspaces\WorkspaceInterface[] $workspaces */
|
||||
$workspaces = $this->entityTypeManager->getStorage('workspace')->loadMultiple();
|
||||
|
||||
// First, sort everything alphabetically.
|
||||
uasort($workspaces, function (WorkspaceInterface $a, WorkspaceInterface $b) {
|
||||
return strnatcasecmp($a->label(), $b->label());
|
||||
});
|
||||
|
||||
$tree_children = [];
|
||||
foreach ($workspaces as $workspace_id => $workspace) {
|
||||
$tree_children[$workspace->parent->target_id][] = $workspace_id;
|
||||
}
|
||||
|
||||
// Keeps track of the parents we have to process, the last entry is used
|
||||
// for the next processing step. Top-level (root) workspace use NULL as
|
||||
// the parent, so we need to initialize the list with that value.
|
||||
$process_parents[] = NULL;
|
||||
|
||||
// Loops over the parent entities and adds its children to the tree array.
|
||||
// Uses a loop instead of a recursion, because it's more efficient.
|
||||
$tree = [];
|
||||
while (count($process_parents)) {
|
||||
$parent = array_pop($process_parents);
|
||||
|
||||
if (!empty($tree_children[$parent])) {
|
||||
$child_id = current($tree_children[$parent]);
|
||||
do {
|
||||
if (empty($child_id)) {
|
||||
break;
|
||||
}
|
||||
$tree[$child_id] = $workspaces[$child_id];
|
||||
|
||||
if (!empty($tree_children[$child_id])) {
|
||||
// We have to continue with this parent later.
|
||||
$process_parents[] = $parent;
|
||||
// Use the current entity as parent for the next iteration.
|
||||
$process_parents[] = $child_id;
|
||||
|
||||
// Move pointer so that we get the correct entity the next time.
|
||||
next($tree_children[$parent]);
|
||||
break;
|
||||
}
|
||||
} while ($child_id = next($tree_children[$parent]));
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a graph object in order to populate the `_depth`, `_ancestors`
|
||||
// and '_descendants' properties for all the entities.
|
||||
$graph = [];
|
||||
foreach ($workspaces as $workspace_id => $workspace) {
|
||||
$graph[$workspace_id]['edges'] = [];
|
||||
if (!$workspace->parent->isEmpty()) {
|
||||
$graph[$workspace_id]['edges'][$workspace->parent->target_id] = TRUE;
|
||||
}
|
||||
}
|
||||
$graph = (new Graph($graph))->searchAndSort();
|
||||
|
||||
foreach (array_keys($tree) as $workspace_id) {
|
||||
$this->tree[$workspace_id] = [
|
||||
'depth' => count($graph[$workspace_id]['paths']),
|
||||
'ancestors' => array_keys($graph[$workspace_id]['paths']),
|
||||
'descendants' => isset($graph[$workspace_id]['reverse_paths']) ? array_keys($graph[$workspace_id]['reverse_paths']) : [],
|
||||
];
|
||||
}
|
||||
|
||||
// Use the 'workspace_list' entity type cache tag because it will be
|
||||
// invalidated automatically when a workspace is added, updated or
|
||||
// deleted.
|
||||
$this->cache->set('workspace_tree', $this->tree, Cache::PERMANENT, $this->entityTypeManager->getDefinition('workspace')->getListCacheTags());
|
||||
}
|
||||
|
||||
return $this->tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getDescendantsAndSelf($workspace_id) {
|
||||
return array_merge([$workspace_id], $this->loadTree()[$workspace_id]['descendants']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function resetCache() {
|
||||
$this->tree = NULL;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\workspaces;
|
||||
|
||||
/**
|
||||
* Provides an interface for workspace tree lookup operations.
|
||||
*/
|
||||
interface WorkspaceRepositoryInterface {
|
||||
|
||||
/**
|
||||
* Returns an array of workspaces tree item properties, sorted in tree order.
|
||||
*
|
||||
* @return array
|
||||
* An array of workspace tree item properties, keyed by the workspace IDs.
|
||||
* The tree item properties are:
|
||||
* - depth: The depth of the workspace in the tree;
|
||||
* - ancestors: The ancestor IDs of the workspace;
|
||||
* - descendants: The descendant IDs of the workspace.
|
||||
*/
|
||||
public function loadTree();
|
||||
|
||||
/**
|
||||
* Returns the descendant IDs of the passed-in workspace, including itself.
|
||||
*
|
||||
* @param string $workspace_id
|
||||
* A workspace ID.
|
||||
*
|
||||
* @return string[]
|
||||
* An array of descendant workspace IDs, including the passed-in one.
|
||||
*/
|
||||
public function getDescendantsAndSelf($workspace_id);
|
||||
|
||||
/**
|
||||
* Resets the cached workspace tree.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function resetCache();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\workspaces;
|
||||
|
||||
use Drupal\path_alias\AliasRepository;
|
||||
|
||||
/**
|
||||
* Provides workspace-specific path alias lookup queries.
|
||||
*/
|
||||
class WorkspacesAliasRepository extends AliasRepository {
|
||||
|
||||
/**
|
||||
* The workspace manager.
|
||||
*
|
||||
* @var \Drupal\workspaces\WorkspaceManagerInterface
|
||||
*/
|
||||
protected $workspaceManager;
|
||||
|
||||
/**
|
||||
* Sets the workspace manager.
|
||||
*
|
||||
* @param \Drupal\workspaces\WorkspaceManagerInterface $workspace_manager
|
||||
* The workspace manager service.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setWorkspacesManager(WorkspaceManagerInterface $workspace_manager) {
|
||||
$this->workspaceManager = $workspace_manager;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getBaseQuery() {
|
||||
// Don't alter any queries if we're not in a workspace context.
|
||||
if (!$this->workspaceManager->hasActiveWorkspace()) {
|
||||
return parent::getBaseQuery();
|
||||
}
|
||||
|
||||
$active_workspace = $this->workspaceManager->getActiveWorkspace();
|
||||
|
||||
$query = $this->connection->select('path_alias', 'base_table_2');
|
||||
$wa_join = $query->leftJoin('workspace_association', NULL, "%alias.target_entity_type_id = 'path_alias' AND %alias.target_entity_id = base_table_2.id AND %alias.workspace = :active_workspace_id", [
|
||||
':active_workspace_id' => $active_workspace->id(),
|
||||
]);
|
||||
$query->innerJoin('path_alias_revision', 'base_table', "%alias.revision_id = COALESCE($wa_join.target_entity_revision_id, base_table_2.revision_id)");
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\workspaces;
|
||||
|
||||
use Drupal\Core\DependencyInjection\ContainerBuilder;
|
||||
use Drupal\Core\DependencyInjection\ServiceProviderBase;
|
||||
use Drupal\Core\Update\UpdateKernel;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
|
||||
/**
|
||||
* Defines a service provider for the Workspaces module.
|
||||
*/
|
||||
class WorkspacesServiceProvider extends ServiceProviderBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function alter(ContainerBuilder $container) {
|
||||
// Add the 'workspace' cache context as required.
|
||||
$renderer_config = $container->getParameter('renderer.config');
|
||||
$renderer_config['required_cache_contexts'][] = 'workspace';
|
||||
$container->setParameter('renderer.config', $renderer_config);
|
||||
|
||||
// Replace the class of the 'path_alias.repository' service.
|
||||
if ($container->hasDefinition('path_alias.repository')) {
|
||||
$definition = $container->getDefinition('path_alias.repository');
|
||||
if (!$definition->isDeprecated()) {
|
||||
$definition
|
||||
->setClass(WorkspacesAliasRepository::class)
|
||||
->addMethodCall('setWorkspacesManager', [new Reference('workspaces.manager')]);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that there's no active workspace while running database updates by
|
||||
// removing the relevant tag from all workspace negotiator services.
|
||||
if ($container->get('kernel') instanceof UpdateKernel) {
|
||||
foreach ($container->getDefinitions() as $id => $definition) {
|
||||
if ($definition->hasTag('workspace_negotiator')) {
|
||||
$definition->clearTag('workspace_negotiator');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
BIN
Binary file not shown.
+8
@@ -0,0 +1,8 @@
|
||||
name: 'Workspace Access Test'
|
||||
type: module
|
||||
description: 'Provides supporting code for testing access for workspaces.'
|
||||
package: Testing
|
||||
version: VERSION
|
||||
core: 8.x
|
||||
dependencies:
|
||||
- drupal:workspaces
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Provides supporting code for testing access for workspaces.
|
||||
*/
|
||||
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
|
||||
/**
|
||||
* Implements hook_ENTITY_TYPE_access() for the 'workspace' entity type.
|
||||
*/
|
||||
function workspace_access_test_workspace_access(EntityInterface $entity, $operation, AccountInterface $account) {
|
||||
return \Drupal::state()->get("workspace_access_test.result.$operation", AccessResult::neutral());
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\workspace_update_test\Negotiator;
|
||||
|
||||
use Drupal\workspaces\Entity\Workspace;
|
||||
use Drupal\workspaces\Negotiator\WorkspaceNegotiatorInterface;
|
||||
use Drupal\workspaces\WorkspaceInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
/**
|
||||
* Defines a workspace negotiator used for testing.
|
||||
*/
|
||||
class TestWorkspaceNegotiator implements WorkspaceNegotiatorInterface {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function applies(Request $request) {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getActiveWorkspace(Request $request) {
|
||||
return Workspace::create(['id' => 'test', 'label' => 'Test']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setActiveWorkspace(WorkspaceInterface $workspace) {
|
||||
// Nothing to do here.
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function unsetActiveWorkspace() {
|
||||
// Nothing to do here.
|
||||
}
|
||||
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
name: 'Workspace Update Test'
|
||||
type: module
|
||||
description: 'Provides supporting code for testing workspaces during database updates.'
|
||||
package: Testing
|
||||
version: VERSION
|
||||
dependencies:
|
||||
- drupal:workspaces
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Post update functions for the Workspace Update Test module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Checks the active workspace during database updates.
|
||||
*/
|
||||
function workspace_update_test_post_update_check_active_workspace() {
|
||||
\Drupal::state()->set('workspace_update_test.has_active_workspace', \Drupal::service('workspaces.manager')->hasActiveWorkspace());
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
services:
|
||||
workspace_update_test.negotiator.test:
|
||||
class: Drupal\workspace_update_test\Negotiator\TestWorkspaceNegotiator
|
||||
tags:
|
||||
- { name: workspace_negotiator, priority: 0 }
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\workspaces\Functional\EntityResource;
|
||||
|
||||
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
|
||||
|
||||
/**
|
||||
* Test workspace entities for unauthenticated JSON requests.
|
||||
*
|
||||
* @group workspaces
|
||||
*/
|
||||
class WorkspaceJsonAnonTest extends WorkspaceResourceTestBase {
|
||||
|
||||
use AnonResourceTestTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $format = 'json';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $mimeType = 'application/json';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\workspaces\Functional\EntityResource;
|
||||
|
||||
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
|
||||
|
||||
/**
|
||||
* Test workspace entities for JSON requests via basic auth.
|
||||
*
|
||||
* @group workspaces
|
||||
*/
|
||||
class WorkspaceJsonBasicAuthTest extends WorkspaceResourceTestBase {
|
||||
|
||||
use BasicAuthResourceTestTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = ['basic_auth'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $format = 'json';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $mimeType = 'application/json';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $auth = 'basic_auth';
|
||||
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\workspaces\Functional\EntityResource;
|
||||
|
||||
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
|
||||
|
||||
/**
|
||||
* Test workspace entities for JSON requests with cookie authentication.
|
||||
*
|
||||
* @group workspaces
|
||||
*/
|
||||
class WorkspaceJsonCookieTest extends WorkspaceResourceTestBase {
|
||||
|
||||
use CookieResourceTestTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $format = 'json';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $mimeType = 'application/json';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $auth = 'cookie';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
}
|
||||
+200
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\workspaces\Functional\EntityResource;
|
||||
|
||||
use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait;
|
||||
use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
|
||||
use Drupal\user\Entity\User;
|
||||
use Drupal\workspaces\Entity\Workspace;
|
||||
|
||||
/**
|
||||
* Base class for workspace EntityResource tests.
|
||||
*/
|
||||
abstract class WorkspaceResourceTestBase extends EntityResourceTestBase {
|
||||
|
||||
use BcTimestampNormalizerUnixTestTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = ['workspaces'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $entityTypeId = 'workspace';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $patchProtectedFieldNames = [
|
||||
'changed' => NULL,
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $firstCreatedEntityId = 'running_on_faith';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $secondCreatedEntityId = 'running_on_faith_2';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUpAuthorization($method) {
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
$this->grantPermissionsToTestedRole(['view any workspace']);
|
||||
break;
|
||||
|
||||
case 'POST':
|
||||
$this->grantPermissionsToTestedRole(['create workspace']);
|
||||
break;
|
||||
|
||||
case 'PATCH':
|
||||
$this->grantPermissionsToTestedRole(['edit any workspace']);
|
||||
break;
|
||||
|
||||
case 'DELETE':
|
||||
$this->grantPermissionsToTestedRole(['delete any workspace']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createEntity() {
|
||||
$workspace = Workspace::create([
|
||||
'id' => 'layla',
|
||||
'label' => 'Layla',
|
||||
]);
|
||||
$workspace->save();
|
||||
return $workspace;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createAnotherEntity() {
|
||||
$workspace = $this->entity->createDuplicate();
|
||||
$workspace->id = 'layla_dupe';
|
||||
$workspace->label = 'Layla_dupe';
|
||||
$workspace->save();
|
||||
return $workspace;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedNormalizedEntity() {
|
||||
$author = User::load($this->entity->getOwnerId());
|
||||
return [
|
||||
'created' => [
|
||||
$this->formatExpectedTimestampItemValues((int) $this->entity->getCreatedTime()),
|
||||
],
|
||||
'changed' => [
|
||||
$this->formatExpectedTimestampItemValues($this->entity->getChangedTime()),
|
||||
],
|
||||
'id' => [
|
||||
[
|
||||
'value' => 'layla',
|
||||
],
|
||||
],
|
||||
'label' => [
|
||||
[
|
||||
'value' => 'Layla',
|
||||
],
|
||||
],
|
||||
'revision_id' => [
|
||||
[
|
||||
'value' => 2,
|
||||
],
|
||||
],
|
||||
'parent' => [],
|
||||
'uid' => [
|
||||
[
|
||||
'target_id' => (int) $author->id(),
|
||||
'target_type' => 'user',
|
||||
'target_uuid' => $author->uuid(),
|
||||
'url' => base_path() . 'user/' . $author->id(),
|
||||
],
|
||||
],
|
||||
'uuid' => [
|
||||
[
|
||||
'value' => $this->entity->uuid(),
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getNormalizedPostEntity() {
|
||||
return [
|
||||
'id' => [
|
||||
[
|
||||
'value' => static::$firstCreatedEntityId,
|
||||
],
|
||||
],
|
||||
'label' => [
|
||||
[
|
||||
'value' => 'Running on faith',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getSecondNormalizedPostEntity() {
|
||||
$normalized_post_entity = $this->getNormalizedPostEntity();
|
||||
$normalized_post_entity['id'][0]['value'] = static::$secondCreatedEntityId;
|
||||
|
||||
return $normalized_post_entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getNormalizedPatchEntity() {
|
||||
return [
|
||||
'label' => [
|
||||
[
|
||||
'value' => 'Running on faith',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getExpectedUnauthorizedAccessMessage($method) {
|
||||
if ($this->config('rest.settings')->get('bc_entity_resource_permissions')) {
|
||||
return parent::getExpectedUnauthorizedAccessMessage($method);
|
||||
}
|
||||
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
return "The 'view any workspace' permission is required.";
|
||||
|
||||
case 'POST':
|
||||
return "The 'create workspace' permission is required.";
|
||||
|
||||
case 'PATCH':
|
||||
return "The 'edit any workspace' permission is required.";
|
||||
|
||||
case 'DELETE':
|
||||
return "The 'delete any workspace' permission is required.";
|
||||
|
||||
}
|
||||
return parent::getExpectedUnauthorizedAccessMessage($method);
|
||||
}
|
||||
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\workspaces\Functional\EntityResource;
|
||||
|
||||
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
|
||||
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
|
||||
|
||||
/**
|
||||
* Test workspace entities for unauthenticated XML requests.
|
||||
*
|
||||
* @group workspaces
|
||||
*/
|
||||
class WorkspaceXmlAnonTest extends WorkspaceResourceTestBase {
|
||||
|
||||
use AnonResourceTestTrait;
|
||||
use XmlEntityNormalizationQuirksTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $format = 'xml';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $mimeType = 'text/xml; charset=UTF-8';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function testPatchPath() {
|
||||
// Deserialization of the XML format is not supported.
|
||||
$this->markTestSkipped();
|
||||
}
|
||||
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\workspaces\Functional\EntityResource;
|
||||
|
||||
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
|
||||
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
|
||||
|
||||
/**
|
||||
* Test workspace entities for XML requests with cookie authentication.
|
||||
*
|
||||
* @group workspaces
|
||||
*/
|
||||
class WorkspaceXmlBasicAuthTest extends WorkspaceResourceTestBase {
|
||||
|
||||
use BasicAuthResourceTestTrait;
|
||||
use XmlEntityNormalizationQuirksTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = ['basic_auth'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $format = 'xml';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $mimeType = 'text/xml; charset=UTF-8';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $auth = 'basic_auth';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function testPatchPath() {
|
||||
// Deserialization of the XML format is not supported.
|
||||
$this->markTestSkipped();
|
||||
}
|
||||
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\workspaces\Functional\EntityResource;
|
||||
|
||||
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
|
||||
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
|
||||
|
||||
/**
|
||||
* Test workspace entities for XML requests.
|
||||
*
|
||||
* @group workspaces
|
||||
*/
|
||||
class WorkspaceXmlCookieTest extends WorkspaceResourceTestBase {
|
||||
|
||||
use CookieResourceTestTrait;
|
||||
use XmlEntityNormalizationQuirksTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $format = 'xml';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $mimeType = 'text/xml; charset=UTF-8';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $auth = 'cookie';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function testPatchPath() {
|
||||
// Deserialization of the XML format is not supported.
|
||||
$this->markTestSkipped();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\workspaces\Functional;
|
||||
|
||||
use Drupal\language\Entity\ConfigurableLanguage;
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
use Drupal\workspaces\Entity\Workspace;
|
||||
|
||||
/**
|
||||
* Tests path aliases with workspaces.
|
||||
*
|
||||
* @group path
|
||||
* @group workspaces
|
||||
*/
|
||||
class PathWorkspacesTest extends BrowserTestBase {
|
||||
|
||||
use WorkspaceTestUtilities;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'block',
|
||||
'content_translation',
|
||||
'node',
|
||||
'path',
|
||||
'workspaces',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
ConfigurableLanguage::createFromLangcode('ro')->save();
|
||||
$this->rebuildContainer();
|
||||
|
||||
// Create a content type.
|
||||
$this->drupalCreateContentType([
|
||||
'name' => 'article',
|
||||
'type' => 'article',
|
||||
]);
|
||||
|
||||
$this->drupalLogin($this->rootUser);
|
||||
|
||||
// Enable URL language detection and selection.
|
||||
$edit = ['language_interface[enabled][language-url]' => 1];
|
||||
$this->drupalPostForm('admin/config/regional/language/detection', $edit, 'Save settings');
|
||||
|
||||
// Enable translation for article node.
|
||||
$edit = [
|
||||
'entity_types[node]' => 1,
|
||||
'settings[node][article][translatable]' => 1,
|
||||
'settings[node][article][fields][path]' => 1,
|
||||
'settings[node][article][fields][body]' => 1,
|
||||
'settings[node][article][settings][language][language_alterable]' => 1,
|
||||
];
|
||||
$this->drupalPostForm('admin/config/regional/content-language', $edit, 'Save configuration');
|
||||
\Drupal::entityTypeManager()->clearCachedDefinitions();
|
||||
|
||||
$this->setupWorkspaceSwitcherBlock();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests path aliases with workspaces.
|
||||
*/
|
||||
public function testPathAliases() {
|
||||
// Create a published node in Live, without an alias.
|
||||
$node = $this->drupalCreateNode([
|
||||
'type' => 'article',
|
||||
'status' => TRUE,
|
||||
]);
|
||||
|
||||
// Switch to Stage and create an alias for the node.
|
||||
$stage = Workspace::load('stage');
|
||||
$this->switchToWorkspace($stage);
|
||||
|
||||
$edit = [
|
||||
'path[0][alias]' => '/' . $this->randomMachineName(),
|
||||
];
|
||||
$this->drupalPostForm('node/' . $node->id() . '/edit', $edit, 'Save');
|
||||
|
||||
// Check that the node can be accessed in Stage with the given alias.
|
||||
$path = $edit['path[0][alias]'];
|
||||
$this->assertAccessiblePaths([$path]);
|
||||
|
||||
// Check that the 'preload-paths' cache includes the active workspace ID in
|
||||
// the cache key.
|
||||
$this->assertNotEmpty(\Drupal::cache('data')->get('preload-paths:stage:/node/1'));
|
||||
$this->assertFalse(\Drupal::cache('data')->get('preload-paths:/node/1'));
|
||||
|
||||
// Check that the alias can not be accessed in Live.
|
||||
$this->switchToLive();
|
||||
$this->assertNotAccessiblePaths([$path]);
|
||||
$this->assertFalse(\Drupal::cache('data')->get('preload-paths:/node/1'));
|
||||
|
||||
// Publish the workspace and check that the alias can be accessed in Live.
|
||||
$stage->publish();
|
||||
$this->assertAccessiblePaths([$path]);
|
||||
$this->assertNotEmpty(\Drupal::cache('data')->get('preload-paths:/node/1'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests path aliases with workspaces and user switching.
|
||||
*/
|
||||
public function testPathAliasesUserSwitch() {
|
||||
// Create a published node in Live, without an alias.
|
||||
$node = $this->drupalCreateNode([
|
||||
'type' => 'article',
|
||||
'status' => TRUE,
|
||||
]);
|
||||
|
||||
// Switch to Stage and create an alias for the node.
|
||||
$stage = Workspace::load('stage');
|
||||
$this->switchToWorkspace($stage);
|
||||
|
||||
$edit = [
|
||||
'path[0][alias]' => '/' . $this->randomMachineName(),
|
||||
];
|
||||
$this->drupalPostForm('node/' . $node->id() . '/edit', $edit, 'Save');
|
||||
|
||||
// Check that the node can be accessed in Stage with the given alias.
|
||||
$path = $edit['path[0][alias]'];
|
||||
$this->assertAccessiblePaths([$path]);
|
||||
|
||||
// Check that the 'preload-paths' cache includes the active workspace ID in
|
||||
// the cache key.
|
||||
$this->assertNotEmpty(\Drupal::cache('data')->get('preload-paths:stage:/node/1'));
|
||||
$this->assertFalse(\Drupal::cache('data')->get('preload-paths:/node/1'));
|
||||
|
||||
// Check that the alias can not be accessed in Live, by logging out without
|
||||
// an explicit switch.
|
||||
$this->drupalLogout();
|
||||
$this->assertNotAccessiblePaths([$path]);
|
||||
$this->assertFalse(\Drupal::cache('data')->get('preload-paths:/node/1'));
|
||||
|
||||
// Publish the workspace and check that the alias can be accessed in Live.
|
||||
$this->drupalLogin($this->rootUser);
|
||||
$stage->publish();
|
||||
$this->drupalLogout();
|
||||
$this->assertAccessiblePaths([$path]);
|
||||
$this->assertNotEmpty(\Drupal::cache('data')->get('preload-paths:/node/1'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests path aliases with workspaces for translatable nodes.
|
||||
*/
|
||||
public function testPathAliasesWithTranslation() {
|
||||
$stage = Workspace::load('stage');
|
||||
|
||||
// Create one node with a random alias.
|
||||
$default_node = $this->drupalCreateNode([
|
||||
'type' => 'article',
|
||||
'langcode' => 'en',
|
||||
'status' => TRUE,
|
||||
'path' => '/' . $this->randomMachineName(),
|
||||
]);
|
||||
|
||||
// Add published translation with another alias.
|
||||
$this->drupalGet('node/' . $default_node->id());
|
||||
$this->drupalGet('node/' . $default_node->id() . '/translations');
|
||||
$this->clickLink('Add');
|
||||
$edit_translation = [
|
||||
'body[0][value]' => $this->randomMachineName(),
|
||||
'status[value]' => TRUE,
|
||||
'path[0][alias]' => '/' . $this->randomMachineName(),
|
||||
];
|
||||
$this->drupalPostForm(NULL, $edit_translation, 'Save (this translation)');
|
||||
// Confirm that the alias works.
|
||||
$this->drupalGet('ro' . $edit_translation['path[0][alias]']);
|
||||
$this->assertSession()->pageTextContains($edit_translation['body[0][value]']);
|
||||
|
||||
$default_path = $default_node->path->alias;
|
||||
$translation_path = 'ro' . $edit_translation['path[0][alias]'];
|
||||
|
||||
$this->assertAccessiblePaths([$default_path, $translation_path]);
|
||||
|
||||
$this->switchToWorkspace($stage);
|
||||
|
||||
$this->assertAccessiblePaths([$default_path, $translation_path]);
|
||||
|
||||
// Create a workspace-specific revision for the translation with a new path
|
||||
// alias.
|
||||
$edit_new_translation_draft_with_alias = [
|
||||
'path[0][alias]' => '/' . $this->randomMachineName(),
|
||||
];
|
||||
$this->drupalPostForm('ro/node/' . $default_node->id() . '/edit', $edit_new_translation_draft_with_alias, 'Save (this translation)');
|
||||
$stage_translation_path = 'ro' . $edit_new_translation_draft_with_alias['path[0][alias]'];
|
||||
|
||||
// The new alias of the translation should be available in Stage, but not
|
||||
// available in Live.
|
||||
$this->assertAccessiblePaths([$default_path, $stage_translation_path]);
|
||||
|
||||
// Check that the previous (Live) path alias no longer works.
|
||||
$this->assertNotAccessiblePaths([$translation_path]);
|
||||
|
||||
// Switch out of Stage and check that the initial path aliases still work.
|
||||
$this->switchToLive();
|
||||
$this->assertAccessiblePaths([$default_path, $translation_path]);
|
||||
$this->assertNotAccessiblePaths([$stage_translation_path]);
|
||||
|
||||
// Switch back to Stage.
|
||||
$this->switchToWorkspace($stage);
|
||||
|
||||
// Create new workspace-specific revision for translation without changing
|
||||
// the path alias.
|
||||
$edit_new_translation_draft = [
|
||||
'body[0][value]' => $this->randomMachineName(),
|
||||
];
|
||||
$this->drupalPostForm('ro/node/' . $default_node->id() . '/edit', $edit_new_translation_draft, t('Save (this translation)'));
|
||||
// Confirm that the new draft revision was created.
|
||||
$this->assertSession()->pageTextContains($edit_new_translation_draft['body[0][value]']);
|
||||
|
||||
// Switch out of Stage and check that the initial path aliases still work.
|
||||
$this->switchToLive();
|
||||
$this->assertAccessiblePaths([$default_path, $translation_path]);
|
||||
$this->assertNotAccessiblePaths([$stage_translation_path]);
|
||||
|
||||
// Switch back to Stage.
|
||||
$this->switchToWorkspace($stage);
|
||||
$this->assertAccessiblePaths([$default_path, $stage_translation_path]);
|
||||
$this->assertNotAccessiblePaths([$translation_path]);
|
||||
|
||||
// Create a new workspace-specific revision for translation with path alias
|
||||
// from the original language's default revision.
|
||||
$edit_new_translation_draft_with_defaults_alias = [
|
||||
'path[0][alias]' => $default_node->path->alias,
|
||||
];
|
||||
$this->drupalPostForm('ro/node/' . $default_node->id() . '/edit', $edit_new_translation_draft_with_defaults_alias, 'Save (this translation)');
|
||||
|
||||
// Switch out of Stage and check that the initial path aliases still work.
|
||||
$this->switchToLive();
|
||||
$this->assertAccessiblePaths([$default_path, $translation_path]);
|
||||
$this->assertNotAccessiblePaths([$stage_translation_path]);
|
||||
|
||||
// Check that only one path alias (the original one) is available in Stage.
|
||||
$this->switchToWorkspace($stage);
|
||||
$this->assertAccessiblePaths([$default_path]);
|
||||
$this->assertNotAccessiblePaths([$translation_path, $stage_translation_path]);
|
||||
|
||||
// Create new workspace-specific revision for translation with a deleted
|
||||
// (empty) path alias.
|
||||
$edit_new_translation_draft_empty_alias = [
|
||||
'body[0][value]' => $this->randomMachineName(),
|
||||
'path[0][alias]' => '',
|
||||
];
|
||||
$this->drupalPostForm('ro/node/' . $default_node->id() . '/edit', $edit_new_translation_draft_empty_alias, 'Save (this translation)');
|
||||
|
||||
// Check that only one path alias (the original one) is available now.
|
||||
$this->switchToLive();
|
||||
$this->assertAccessiblePaths([$default_path, $translation_path]);
|
||||
$this->assertNotAccessiblePaths([$stage_translation_path]);
|
||||
|
||||
$this->switchToWorkspace($stage);
|
||||
$this->assertAccessiblePaths([$default_path]);
|
||||
$this->assertNotAccessiblePaths([$translation_path, $stage_translation_path]);
|
||||
|
||||
// Create a new workspace-specific revision for the translation with a new
|
||||
// path alias.
|
||||
$edit_new_translation = [
|
||||
'body[0][value]' => $this->randomMachineName(),
|
||||
'path[0][alias]' => '/' . $this->randomMachineName(),
|
||||
];
|
||||
$this->drupalPostForm('ro/node/' . $default_node->id() . '/edit', $edit_new_translation, 'Save (this translation)');
|
||||
|
||||
// Confirm that the new revision was created.
|
||||
$this->assertSession()->pageTextContains($edit_new_translation['body[0][value]']);
|
||||
$this->assertSession()->addressEquals('ro' . $edit_new_translation['path[0][alias]']);
|
||||
|
||||
// Check that only the new path alias of the translation can be accessed.
|
||||
$new_stage_translation_path = 'ro' . $edit_new_translation['path[0][alias]'];
|
||||
$this->assertAccessiblePaths([$default_path, $new_stage_translation_path]);
|
||||
$this->assertNotAccessiblePaths([$stage_translation_path]);
|
||||
|
||||
// Switch out of Stage and check that none of the workspace-specific path
|
||||
// aliases can be accessed.
|
||||
$this->switchToLive();
|
||||
$this->assertAccessiblePaths([$default_path, $translation_path]);
|
||||
$this->assertNotAccessiblePaths([$stage_translation_path, $new_stage_translation_path]);
|
||||
|
||||
// Publish Stage and check that its path alias for the translation can be
|
||||
// accessed.
|
||||
$stage->publish();
|
||||
$this->assertAccessiblePaths([$default_path, $new_stage_translation_path]);
|
||||
$this->assertNotAccessiblePaths([$stage_translation_path]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper callback to verify paths are responding with status 200.
|
||||
*
|
||||
* @param string[] $paths
|
||||
* An array of paths to check for.
|
||||
*/
|
||||
protected function assertAccessiblePaths(array $paths) {
|
||||
foreach ($paths as $path) {
|
||||
$this->drupalGet($path);
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper callback to verify paths are responding with status 404.
|
||||
*
|
||||
* @param string[] $paths
|
||||
* An array of paths to check for.
|
||||
*/
|
||||
protected function assertNotAccessiblePaths(array $paths) {
|
||||
foreach ($paths as $path) {
|
||||
$this->drupalGet($path);
|
||||
$this->assertSession()->statusCodeEquals(404);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\workspaces\Functional\Update;
|
||||
|
||||
use Drupal\Core\Entity\Entity\EntityFormDisplay;
|
||||
use Drupal\FunctionalTests\Update\UpdatePathTestBase;
|
||||
use Drupal\workspaces\Entity\Workspace;
|
||||
|
||||
/**
|
||||
* Tests the upgrade path for the Workspaces module.
|
||||
*
|
||||
* @group workspaces
|
||||
* @group Update
|
||||
* @group legacy
|
||||
*/
|
||||
class WorkspacesUpdateTest extends UpdatePathTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['workspaces', 'workspace_update_test'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setDatabaseDumpFiles() {
|
||||
$this->databaseDumpFiles = [
|
||||
__DIR__ . '/../../../../../system/tests/fixtures/update/drupal-8.4.0.bare.standard.php.gz',
|
||||
__DIR__ . '/../../../fixtures/update/drupal-8.6.0-workspaces_installed.php',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the move of workspace association data to a custom table.
|
||||
*
|
||||
* @see workspaces_update_8801()
|
||||
* @see workspaces_post_update_move_association_data()
|
||||
*/
|
||||
public function testWorkspaceAssociationRemoval() {
|
||||
$database = \Drupal::database();
|
||||
|
||||
// Check that we have two records in the 'workspace_association' base table
|
||||
// and three records in its revision table.
|
||||
$wa_records = $database->select('workspace_association')->countQuery()->execute()->fetchField();
|
||||
$this->assertEquals(2, $wa_records);
|
||||
$war_records = $database->select('workspace_association_revision')->countQuery()->execute()->fetchField();
|
||||
$this->assertEquals(3, $war_records);
|
||||
|
||||
// Check that the node entity type does not have a 'workspace' field.
|
||||
$this->assertNull(\Drupal::entityDefinitionUpdateManager()->getFieldStorageDefinition('workspace', 'node'));
|
||||
|
||||
$this->runUpdates();
|
||||
|
||||
$entity_definition_update_manager = \Drupal::entityDefinitionUpdateManager();
|
||||
|
||||
// Check that the 'workspace' field has been installed for an entity type
|
||||
// that was workspace-supported before Drupal 8.7.0.
|
||||
$this->assertTrue($entity_definition_update_manager->getFieldStorageDefinition('workspace', 'node'));
|
||||
|
||||
// Check that the 'workspace' field has been installed for an entity type
|
||||
// which became workspace-supported as part of an entity schema update.
|
||||
$this->assertTrue($entity_definition_update_manager->getFieldStorageDefinition('workspace', 'taxonomy_term'));
|
||||
|
||||
// Check that the 'workspace' field has been installed for an entity type
|
||||
// that has been added in an update function.
|
||||
$this->assertTrue($entity_definition_update_manager->getFieldStorageDefinition('workspace', 'path_alias'));
|
||||
|
||||
// Check that the 'workspace' revision metadata field has been created only
|
||||
// in the revision table.
|
||||
$schema = $database->schema();
|
||||
$this->assertTrue($schema->fieldExists('node_revision', 'workspace'));
|
||||
$this->assertFalse($schema->fieldExists('node', 'workspace'));
|
||||
$this->assertFalse($schema->fieldExists('node_field_data', 'workspace'));
|
||||
$this->assertFalse($schema->fieldExists('node_field_revision', 'workspace'));
|
||||
|
||||
// Check that the 'workspace_association' records have been migrated
|
||||
// properly.
|
||||
$wa_records = $database->select('workspace_association')->fields('workspace_association')->execute()->fetchAll(\PDO::FETCH_ASSOC);
|
||||
$expected = [
|
||||
[
|
||||
'workspace' => 'stage',
|
||||
'target_entity_type_id' => 'node',
|
||||
'target_entity_id' => '1',
|
||||
'target_entity_revision_id' => '2',
|
||||
],
|
||||
[
|
||||
'workspace' => 'dev',
|
||||
'target_entity_type_id' => 'node',
|
||||
'target_entity_id' => '8',
|
||||
'target_entity_revision_id' => '10',
|
||||
],
|
||||
];
|
||||
$this->assertEquals($expected, $wa_records);
|
||||
|
||||
// Check that the 'workspace_association' revisions has been migrated
|
||||
// properly to the new 'workspace' revision metadata field.
|
||||
$revisions = \Drupal::entityTypeManager()->getStorage('node')->loadMultipleRevisions([2, 9, 10]);
|
||||
$this->assertEquals('stage', $revisions[2]->workspace->target_id);
|
||||
$this->assertEquals('dev', $revisions[9]->workspace->target_id);
|
||||
$this->assertEquals('dev', $revisions[10]->workspace->target_id);
|
||||
|
||||
// Check that the 'workspace_association' entity type has been uninstalled.
|
||||
$this->assertNull($entity_definition_update_manager->getEntityType('workspace_association'));
|
||||
$this->assertNull($entity_definition_update_manager->getFieldStorageDefinition('id', 'workspace_association'));
|
||||
$this->assertNull(\Drupal::keyValue('entity.storage_schema.sql')->get('workspace_association.entity_schema_data'));
|
||||
|
||||
// Check that the 'workspace_association_revision' table has been removed.
|
||||
$this->assertFalse($schema->tableExists('workspace_association_revision'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the addition of the workspace 'parent' field.
|
||||
*
|
||||
* @see workspaces_update_8802()
|
||||
* @see workspaces_post_update_update_deploy_form_display()
|
||||
*/
|
||||
public function testWorkspaceParentField() {
|
||||
$this->runUpdates();
|
||||
|
||||
$this->assertNotEmpty(\Drupal::entityDefinitionUpdateManager()->getFieldStorageDefinition('parent', 'workspace'));
|
||||
$stage = Workspace::load('stage');
|
||||
$this->assertTrue($stage->hasField('parent'));
|
||||
$this->assertTrue($stage->parent->isEmpty());
|
||||
|
||||
// Check that the 'parent' field is hidden in the Deploy form display.
|
||||
$form_display = EntityFormDisplay::load('workspace.workspace.deploy');
|
||||
$this->assertNull($form_display->getComponent('parent'));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function replaceUser1() {
|
||||
// Do not replace the user from our dump.
|
||||
}
|
||||
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\workspaces\Functional\UpdateSystem;
|
||||
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
use Drupal\Tests\UpdatePathTestTrait;
|
||||
|
||||
/**
|
||||
* Tests that there is no active workspace during database updates.
|
||||
*
|
||||
* @group workspaces
|
||||
* @group Update
|
||||
*/
|
||||
class ActiveWorkspaceUpdateTest extends BrowserTestBase {
|
||||
use UpdatePathTestTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['workspaces', 'workspace_update_test'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
// Ensure the workspace_update_test_post_update_check_active_workspace()
|
||||
// update runs.
|
||||
$existing_updates = \Drupal::keyValue('post_update')->get('existing_updates', []);
|
||||
$index = array_search('workspace_update_test_post_update_check_active_workspace', $existing_updates);
|
||||
unset($existing_updates[$index]);
|
||||
\Drupal::keyValue('post_update')->set('existing_updates', $existing_updates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that there is no active workspace during database updates.
|
||||
*/
|
||||
public function testActiveWorkspaceDuringUpdate() {
|
||||
/** @var \Drupal\workspaces\WorkspaceManagerInterface $workspace_manager */
|
||||
$workspace_manager = \Drupal::service('workspaces.manager');
|
||||
|
||||
// Check that we have an active workspace before running the updates.
|
||||
$this->assertTrue($workspace_manager->hasActiveWorkspace());
|
||||
$this->assertEquals('test', $workspace_manager->getActiveWorkspace()->id());
|
||||
|
||||
$this->runUpdates();
|
||||
|
||||
// Check that we didn't have an active workspace while running the updates.
|
||||
// @see workspace_update_test_post_update_check_active_workspace()
|
||||
$this->assertFalse(\Drupal::state()->get('workspace_update_test.has_active_workspace'));
|
||||
|
||||
// Check that we have an active workspace after running the updates.
|
||||
$workspace_manager = \Drupal::service('workspaces.manager');
|
||||
$this->assertTrue($workspace_manager->hasActiveWorkspace());
|
||||
$this->assertEquals('test', $workspace_manager->getActiveWorkspace()->id());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\workspaces\Functional;
|
||||
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
|
||||
|
||||
/**
|
||||
* Tests access bypass permission controls on workspaces.
|
||||
*
|
||||
* @group workspaces
|
||||
*/
|
||||
class WorkspaceBypassTest extends BrowserTestBase {
|
||||
|
||||
use WorkspaceTestUtilities;
|
||||
use ContentTypeCreationTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = ['node', 'user', 'block', 'workspaces'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* Verifies that a user can edit anything in a workspace they own.
|
||||
*/
|
||||
public function testBypassOwnWorkspace() {
|
||||
$permissions = [
|
||||
'create workspace',
|
||||
'edit own workspace',
|
||||
'view own workspace',
|
||||
'bypass entity access own workspace',
|
||||
];
|
||||
|
||||
$this->createContentType(['type' => 'test', 'label' => 'Test']);
|
||||
$this->setupWorkspaceSwitcherBlock();
|
||||
|
||||
$ditka = $this->drupalCreateUser(array_merge($permissions, ['create test content']));
|
||||
|
||||
// Login as a limited-access user and create a workspace.
|
||||
$this->drupalLogin($ditka);
|
||||
$bears = $this->createWorkspaceThroughUi('Bears', 'bears');
|
||||
$this->switchToWorkspace($bears);
|
||||
|
||||
// Now create a node in the Bears workspace, as the owner of that workspace.
|
||||
$ditka_bears_node = $this->createNodeThroughUi('Ditka Bears node', 'test');
|
||||
$ditka_bears_node_id = $ditka_bears_node->id();
|
||||
|
||||
// Editing both nodes should be possible.
|
||||
$this->drupalGet('/node/' . $ditka_bears_node_id . '/edit');
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
|
||||
// Create a new user that should be able to edit anything in the Bears
|
||||
// workspace.
|
||||
$lombardi = $this->drupalCreateUser(array_merge($permissions, ['view any workspace']));
|
||||
$this->drupalLogin($lombardi);
|
||||
$this->switchToWorkspace($bears);
|
||||
|
||||
// Editor 2 has the bypass permission but does not own the workspace and so,
|
||||
// should not be able to create and edit any node.
|
||||
$this->drupalGet('/node/' . $ditka_bears_node_id . '/edit');
|
||||
$this->assertSession()->statusCodeEquals(403);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\workspaces\Functional;
|
||||
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait;
|
||||
use Drupal\workspaces\Entity\Workspace;
|
||||
use Drupal\workspaces\WorkspaceCacheContext;
|
||||
|
||||
/**
|
||||
* Tests the workspace cache context.
|
||||
*
|
||||
* @group workspaces
|
||||
* @group Cache
|
||||
*/
|
||||
class WorkspaceCacheContextTest extends BrowserTestBase {
|
||||
|
||||
use AssertPageCacheContextsAndTagsTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = ['block', 'node', 'workspaces'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* Tests the 'workspace' cache context.
|
||||
*/
|
||||
public function testWorkspaceCacheContext() {
|
||||
$this->dumpHeaders = TRUE;
|
||||
|
||||
$renderer = \Drupal::service('renderer');
|
||||
$cache_contexts_manager = \Drupal::service("cache_contexts_manager");
|
||||
|
||||
// Check that the 'workspace' cache context is present when the module is
|
||||
// installed.
|
||||
$this->drupalGet('<front>');
|
||||
$this->assertCacheContext('workspace');
|
||||
|
||||
$cache_context = new WorkspaceCacheContext(\Drupal::service('workspaces.manager'));
|
||||
$this->assertSame('live', $cache_context->getContext());
|
||||
|
||||
// Create a node and check that its render array contains the proper cache
|
||||
// context.
|
||||
$this->drupalCreateContentType(['type' => 'page']);
|
||||
$node = $this->createNode();
|
||||
|
||||
// Get a fully built entity view render array.
|
||||
$build = \Drupal::entityTypeManager()->getViewBuilder('node')->view($node, 'full');
|
||||
|
||||
// Render it so the default cache contexts are applied.
|
||||
$renderer->renderRoot($build);
|
||||
$this->assertContains('workspace', $build['#cache']['contexts']);
|
||||
|
||||
$cid_parts = array_merge($build['#cache']['keys'], $cache_contexts_manager->convertTokensToKeys($build['#cache']['contexts'])->getKeys());
|
||||
$this->assertContains('[workspace]=live', $cid_parts);
|
||||
|
||||
// Test that a cache entry is created.
|
||||
$cid = implode(':', $cid_parts);
|
||||
$bin = $build['#cache']['bin'];
|
||||
$this->assertInstanceOf(\stdClass::class, $this->container->get('cache.' . $bin)->get($cid));
|
||||
|
||||
// Switch to the 'stage' workspace and check that the correct workspace
|
||||
// cache context is used.
|
||||
$test_user = $this->drupalCreateUser(['view any workspace']);
|
||||
$this->drupalLogin($test_user);
|
||||
|
||||
$stage = Workspace::load('stage');
|
||||
$workspace_manager = \Drupal::service('workspaces.manager');
|
||||
$workspace_manager->setActiveWorkspace($stage);
|
||||
|
||||
$cache_context = new WorkspaceCacheContext($workspace_manager);
|
||||
$this->assertSame('stage', $cache_context->getContext());
|
||||
|
||||
$build = \Drupal::entityTypeManager()->getViewBuilder('node')->view($node, 'full');
|
||||
|
||||
// Render it so the default cache contexts are applied.
|
||||
$renderer->renderRoot($build);
|
||||
$this->assertContains('workspace', $build['#cache']['contexts']);
|
||||
|
||||
$cid_parts = array_merge($build['#cache']['keys'], $cache_contexts_manager->convertTokensToKeys($build['#cache']['contexts'])->getKeys());
|
||||
$this->assertContains('[workspace]=stage', $cid_parts);
|
||||
|
||||
// Test that a cache entry is created.
|
||||
$cid = implode(':', $cid_parts);
|
||||
$bin = $build['#cache']['bin'];
|
||||
$this->assertInstanceOf(\stdClass::class, $this->container->get('cache.' . $bin)->get($cid));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\workspaces\Functional;
|
||||
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
|
||||
/**
|
||||
* Tests concurrent edits in different workspaces.
|
||||
*
|
||||
* @group workspaces
|
||||
*/
|
||||
class WorkspaceConcurrentEditingTest extends BrowserTestBase {
|
||||
|
||||
use WorkspaceTestUtilities;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = ['block', 'node', 'workspaces'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* Test editing a node in multiple workspaces.
|
||||
*/
|
||||
public function testConcurrentEditing() {
|
||||
// Create a test node.
|
||||
$this->createContentType(['type' => 'test', 'label' => 'Test']);
|
||||
$this->setupWorkspaceSwitcherBlock();
|
||||
|
||||
$permissions = [
|
||||
'create workspace',
|
||||
'edit own workspace',
|
||||
'view own workspace',
|
||||
'create test content',
|
||||
'edit own test content',
|
||||
];
|
||||
$mayer = $this->drupalCreateUser($permissions);
|
||||
$this->drupalLogin($mayer);
|
||||
|
||||
$test_node = $this->createNodeThroughUi('Test node', 'test');
|
||||
|
||||
// Check that the user can edit the node.
|
||||
$page = $this->getSession()->getPage();
|
||||
$page->hasField('title[0][value]');
|
||||
|
||||
// Create two workspaces.
|
||||
$vultures = $this->createWorkspaceThroughUi('Vultures', 'vultures');
|
||||
$gravity = $this->createWorkspaceThroughUi('Gravity', 'gravity');
|
||||
|
||||
// Edit the node in workspace 'vultures'.
|
||||
$this->switchToWorkspace($vultures);
|
||||
$this->drupalGet('/node/' . $test_node->id() . '/edit');
|
||||
$page = $this->getSession()->getPage();
|
||||
$page->fillField('Title', 'Test node - override');
|
||||
$page->findButton('Save')->click();
|
||||
|
||||
// Check that the user can still edit the node in the same workspace.
|
||||
$this->drupalGet('/node/' . $test_node->id() . '/edit');
|
||||
$page = $this->getSession()->getPage();
|
||||
$this->assertTrue($page->hasField('title[0][value]'));
|
||||
|
||||
// Switch to a different workspace and check that the user can not edit the
|
||||
// node anymore.
|
||||
$this->switchToWorkspace($gravity);
|
||||
$this->drupalGet('/node/' . $test_node->id() . '/edit');
|
||||
$page = $this->getSession()->getPage();
|
||||
$this->assertFalse($page->hasField('title[0][value]'));
|
||||
$page->hasContent('The content is being edited in the Vultures workspace. As a result, your changes cannot be saved.');
|
||||
|
||||
// Check that the node fails validation for API calls.
|
||||
$violations = $test_node->validate();
|
||||
$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 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]'));
|
||||
$page->hasContent('The content is being edited in the Vultures workspace. As a result, your changes cannot be saved.');
|
||||
|
||||
// Check that the node fails validation for API calls.
|
||||
$violations = $test_node->validate();
|
||||
$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());
|
||||
|
||||
// Deploy the changes from the 'Vultures' workspace and check that the node
|
||||
// can be edited again in other workspaces.
|
||||
$vultures->publish();
|
||||
$this->switchToWorkspace($gravity);
|
||||
$this->drupalGet('/node/' . $test_node->id() . '/edit');
|
||||
$page = $this->getSession()->getPage();
|
||||
$this->assertTrue($page->hasField('title[0][value]'));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\workspaces\Functional;
|
||||
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
use Drupal\workspaces\Entity\Workspace;
|
||||
|
||||
/**
|
||||
* Tests permission controls on workspaces.
|
||||
*
|
||||
* @group workspaces
|
||||
*/
|
||||
class WorkspacePermissionsTest extends BrowserTestBase {
|
||||
|
||||
use WorkspaceTestUtilities;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = ['workspaces'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* Verifies that a user can create but not edit a workspace.
|
||||
*/
|
||||
public function testCreateWorkspace() {
|
||||
$editor = $this->drupalCreateUser([
|
||||
'access administration pages',
|
||||
'administer site configuration',
|
||||
'create workspace',
|
||||
]);
|
||||
|
||||
// Login as a limited-access user and create a workspace.
|
||||
$this->drupalLogin($editor);
|
||||
$this->createWorkspaceThroughUi('Bears', 'bears');
|
||||
|
||||
// Now edit that same workspace; We shouldn't be able to do so, since
|
||||
// we don't have edit permissions.
|
||||
/** @var \Drupal\Core\Entity\EntityTypeManagerInterface $etm */
|
||||
$etm = \Drupal::service('entity_type.manager');
|
||||
/** @var \Drupal\workspaces\WorkspaceInterface $bears */
|
||||
$entity_list = $etm->getStorage('workspace')->loadByProperties(['label' => 'Bears']);
|
||||
$bears = current($entity_list);
|
||||
|
||||
$this->drupalGet("/admin/config/workflow/workspaces/manage/{$bears->id()}/edit");
|
||||
$this->assertSession()->statusCodeEquals(403);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that a user can create and edit only their own workspace.
|
||||
*/
|
||||
public function testEditOwnWorkspace() {
|
||||
$permissions = [
|
||||
'access administration pages',
|
||||
'administer site configuration',
|
||||
'create workspace',
|
||||
'edit own workspace',
|
||||
];
|
||||
|
||||
$editor1 = $this->drupalCreateUser($permissions);
|
||||
|
||||
// Login as a limited-access user and create a workspace.
|
||||
$this->drupalLogin($editor1);
|
||||
$this->createWorkspaceThroughUi('Bears', 'bears');
|
||||
|
||||
// Now edit that same workspace; We should be able to do so.
|
||||
$bears = Workspace::load('bears');
|
||||
|
||||
$this->drupalGet("/admin/config/workflow/workspaces/manage/{$bears->id()}/edit");
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
|
||||
$page = $this->getSession()->getPage();
|
||||
$page->fillField('label', 'Bears again');
|
||||
$page->fillField('id', 'bears');
|
||||
$page->findButton('Save')->click();
|
||||
$page->hasContent('Bears again (bears)');
|
||||
|
||||
// Now login as a different user and ensure they don't have edit access,
|
||||
// and vice versa.
|
||||
$editor2 = $this->drupalCreateUser($permissions);
|
||||
|
||||
$this->drupalLogin($editor2);
|
||||
$this->createWorkspaceThroughUi('Packers', 'packers');
|
||||
$packers = Workspace::load('packers');
|
||||
|
||||
$this->drupalGet("/admin/config/workflow/workspaces/manage/{$packers->id()}/edit");
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
|
||||
$this->drupalGet("/admin/config/workflow/workspaces/manage/{$bears->id()}/edit");
|
||||
$this->assertSession()->statusCodeEquals(403);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that a user can edit any workspace.
|
||||
*/
|
||||
public function testEditAnyWorkspace() {
|
||||
$permissions = [
|
||||
'access administration pages',
|
||||
'administer site configuration',
|
||||
'create workspace',
|
||||
'edit own workspace',
|
||||
];
|
||||
|
||||
$editor1 = $this->drupalCreateUser($permissions);
|
||||
|
||||
// Login as a limited-access user and create a workspace.
|
||||
$this->drupalLogin($editor1);
|
||||
$this->createWorkspaceThroughUi('Bears', 'bears');
|
||||
|
||||
// Now edit that same workspace; We should be able to do so.
|
||||
$bears = Workspace::load('bears');
|
||||
|
||||
$this->drupalGet("/admin/config/workflow/workspaces/manage/{$bears->id()}/edit");
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
|
||||
$page = $this->getSession()->getPage();
|
||||
$page->fillField('label', 'Bears again');
|
||||
$page->fillField('id', 'bears');
|
||||
$page->findButton('Save')->click();
|
||||
$page->hasContent('Bears again (bears)');
|
||||
|
||||
// Now login as a different user and ensure they don't have edit access,
|
||||
// and vice versa.
|
||||
$admin = $this->drupalCreateUser(array_merge($permissions, ['edit any workspace']));
|
||||
|
||||
$this->drupalLogin($admin);
|
||||
$this->createWorkspaceThroughUi('Packers', 'packers');
|
||||
$packers = Workspace::load('packers');
|
||||
|
||||
$this->drupalGet("/admin/config/workflow/workspaces/manage/{$packers->id()}/edit");
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
|
||||
$this->drupalGet("/admin/config/workflow/workspaces/manage/{$bears->id()}/edit");
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that a user can create and delete only their own workspace.
|
||||
*/
|
||||
public function testDeleteOwnWorkspace() {
|
||||
$permissions = [
|
||||
'access administration pages',
|
||||
'administer site configuration',
|
||||
'create workspace',
|
||||
'delete own workspace',
|
||||
];
|
||||
$editor1 = $this->drupalCreateUser($permissions);
|
||||
|
||||
// Login as a limited-access user and create a workspace.
|
||||
$this->drupalLogin($editor1);
|
||||
$bears = $this->createWorkspaceThroughUi('Bears', 'bears');
|
||||
|
||||
// Now try to delete that same workspace; We should be able to do so.
|
||||
$this->drupalGet("/admin/config/workflow/workspaces/manage/{$bears->id()}/delete");
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
|
||||
// Now login as a different user and ensure they don't have edit access,
|
||||
// and vice versa.
|
||||
$editor2 = $this->drupalCreateUser($permissions);
|
||||
|
||||
$this->drupalLogin($editor2);
|
||||
$packers = $this->createWorkspaceThroughUi('Packers', 'packers');
|
||||
|
||||
$this->drupalGet("/admin/config/workflow/workspaces/manage/{$packers->id()}/delete");
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
|
||||
$this->drupalGet("/admin/config/workflow/workspaces/manage/{$bears->id()}/delete");
|
||||
$this->assertSession()->statusCodeEquals(403);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that a user can delete any workspace.
|
||||
*/
|
||||
public function testDeleteAnyWorkspace() {
|
||||
$permissions = [
|
||||
'access administration pages',
|
||||
'administer site configuration',
|
||||
'create workspace',
|
||||
'delete own workspace',
|
||||
];
|
||||
$editor1 = $this->drupalCreateUser($permissions);
|
||||
|
||||
// Login as a limited-access user and create a workspace.
|
||||
$this->drupalLogin($editor1);
|
||||
$bears = $this->createWorkspaceThroughUi('Bears', 'bears');
|
||||
|
||||
// Now edit that same workspace; We should be able to do so.
|
||||
$this->drupalGet("/admin/config/workflow/workspaces/manage/{$bears->id()}/delete");
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
|
||||
// Now login as a different user and ensure they have delete access on both
|
||||
// workspaces.
|
||||
$admin = $this->drupalCreateUser(array_merge($permissions, ['delete any workspace']));
|
||||
|
||||
$this->drupalLogin($admin);
|
||||
$packers = $this->createWorkspaceThroughUi('Packers', 'packers');
|
||||
|
||||
$this->drupalGet("/admin/config/workflow/workspaces/manage/{$packers->id()}/delete");
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
|
||||
$this->drupalGet("/admin/config/workflow/workspaces/manage/{$bears->id()}/delete");
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
}
|
||||
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\workspaces\Functional;
|
||||
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
|
||||
/**
|
||||
* Tests the addition of the revision metadata key.
|
||||
*
|
||||
* @group workspaces
|
||||
* @group legacy
|
||||
*/
|
||||
class WorkspaceRevisionMetadataFieldTest extends BrowserTestBase {
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['entity_test_revlog'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->entityTypeManager = \Drupal::entityTypeManager();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the addition of the 'workspaces' revision metadata field.
|
||||
*
|
||||
* @expectedDeprecation The revision_user revision metadata key is not set for entity type: entity_test_mul_revlog_pub See: https://www.drupal.org/node/2831499
|
||||
* @expectedDeprecation The revision_created revision metadata key is not set for entity type: entity_test_mul_revlog_pub See: https://www.drupal.org/node/2831499
|
||||
* @expectedDeprecation The revision_log_message revision metadata key is not set for entity type: entity_test_mul_revlog_pub See: https://www.drupal.org/node/2831499
|
||||
*/
|
||||
public function testWorkspacesField() {
|
||||
$revision_metadata_keys_prev_live = $this->entityTypeManager->getDefinition('entity_test_mul_revlog_pub')
|
||||
->getRevisionMetadataKeys();
|
||||
$revision_metadata_keys_prev_installed = $this->entityTypeManager->getActiveDefinition('entity_test_mul_revlog_pub')
|
||||
->getRevisionMetadataKeys();
|
||||
|
||||
$this->container->get('module_installer')->install(['workspaces']);
|
||||
$this->entityTypeManager->clearCachedDefinitions();
|
||||
|
||||
$revision_metadata_keys_current_live = $this->entityTypeManager->getDefinition('entity_test_mul_revlog_pub')
|
||||
->getRevisionMetadataKeys();
|
||||
$revision_metadata_keys_current_installed = $this->entityTypeManager->getActiveDefinition('entity_test_mul_revlog_pub')
|
||||
->getRevisionMetadataKeys();
|
||||
|
||||
// Ensure that the live revision metadata keys have changed only by the
|
||||
// workspace revision metadata key.
|
||||
$expected_revision_metadata_keys_live = $revision_metadata_keys_prev_live + ['workspace' => 'workspace'];
|
||||
asort($expected_revision_metadata_keys_live);
|
||||
asort($revision_metadata_keys_current_live);
|
||||
$this->assertEquals($expected_revision_metadata_keys_live, $revision_metadata_keys_current_live);
|
||||
|
||||
// Ensure that the installed revision metadata keys have changed only by the
|
||||
// workspace revision metadata key.
|
||||
$expected_revision_metadata_keys_installed = $revision_metadata_keys_prev_installed + ['workspace' => 'workspace'];
|
||||
asort($expected_revision_metadata_keys_installed);
|
||||
asort($revision_metadata_keys_current_installed);
|
||||
$this->assertEquals($expected_revision_metadata_keys_installed, $revision_metadata_keys_current_installed);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\workspaces\Functional;
|
||||
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
use Drupal\Tests\system\Functional\Cache\AssertPageCacheContextsAndTagsTrait;
|
||||
|
||||
/**
|
||||
* Tests workspace switching functionality.
|
||||
*
|
||||
* @group workspaces
|
||||
*/
|
||||
class WorkspaceSwitcherTest extends BrowserTestBase {
|
||||
|
||||
use AssertPageCacheContextsAndTagsTrait;
|
||||
use WorkspaceTestUtilities;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = ['block', 'workspaces'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'classy';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$permissions = [
|
||||
'create workspace',
|
||||
'edit own workspace',
|
||||
'view own workspace',
|
||||
'bypass entity access own workspace',
|
||||
];
|
||||
|
||||
$this->setupWorkspaceSwitcherBlock();
|
||||
|
||||
$mayer = $this->drupalCreateUser($permissions);
|
||||
$this->drupalLogin($mayer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test switching workspace via the switcher block and admin page.
|
||||
*/
|
||||
public function testSwitchingWorkspaces() {
|
||||
$vultures = $this->createWorkspaceThroughUi('Vultures', 'vultures');
|
||||
$this->switchToWorkspace($vultures);
|
||||
|
||||
$gravity = $this->createWorkspaceThroughUi('Gravity', 'gravity');
|
||||
|
||||
$this->drupalGet('/admin/config/workflow/workspaces/manage/' . $gravity->id() . '/activate');
|
||||
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
$page = $this->getSession()->getPage();
|
||||
$page->findButton('Confirm')->click();
|
||||
|
||||
// Check that WorkspaceCacheContext provides the cache context used to
|
||||
// support its functionality.
|
||||
$this->assertCacheContext('session');
|
||||
|
||||
$page->findLink($gravity->label());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test switching workspace via a query parameter.
|
||||
*/
|
||||
public function testQueryParameterNegotiator() {
|
||||
$web_assert = $this->assertSession();
|
||||
// Initially the default workspace should be active.
|
||||
$web_assert->elementContains('css', '.block-workspace-switcher', 'None');
|
||||
|
||||
// When adding a query parameter the workspace will be switched.
|
||||
$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($current_user_url);
|
||||
$web_assert->elementContains('css', '.block-workspace-switcher', 'Stage');
|
||||
|
||||
// Check that WorkspaceCacheContext provides the cache context used to
|
||||
// support its functionality.
|
||||
$this->assertCacheContext('session');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\workspaces\Functional;
|
||||
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
|
||||
/**
|
||||
* Test the workspace entity.
|
||||
*
|
||||
* @group workspaces
|
||||
*/
|
||||
class WorkspaceTest extends BrowserTestBase {
|
||||
|
||||
use WorkspaceTestUtilities;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = ['workspaces', 'toolbar', 'field_ui'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* A test user.
|
||||
*
|
||||
* @var \Drupal\user\Entity\User
|
||||
*/
|
||||
protected $editor1;
|
||||
|
||||
/**
|
||||
* A test user.
|
||||
*
|
||||
* @var \Drupal\user\Entity\User
|
||||
*/
|
||||
protected $editor2;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setUp() {
|
||||
parent::setUp();
|
||||
$permissions = [
|
||||
'access administration pages',
|
||||
'administer site configuration',
|
||||
'create workspace',
|
||||
'edit own workspace',
|
||||
'edit any workspace',
|
||||
'view own workspace',
|
||||
'access toolbar',
|
||||
];
|
||||
|
||||
$this->editor1 = $this->drupalCreateUser($permissions);
|
||||
$this->editor2 = $this->drupalCreateUser($permissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test creating a workspace with special characters.
|
||||
*/
|
||||
public function testSpecialCharacters() {
|
||||
$this->drupalLogin($this->editor1);
|
||||
|
||||
// Test a valid workspace name.
|
||||
$this->createWorkspaceThroughUi('Workspace 1', 'a0_$()+-/');
|
||||
|
||||
// Test and invalid workspace name.
|
||||
$this->drupalGet('/admin/config/workflow/workspaces/add');
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
|
||||
$page = $this->getSession()->getPage();
|
||||
$page->fillField('label', 'workspace2');
|
||||
$page->fillField('id', 'A!"£%^&*{}#~@?');
|
||||
$page->findButton('Save')->click();
|
||||
$page->hasContent("This value is not valid");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that the toolbar correctly shows the active workspace.
|
||||
*/
|
||||
public function testWorkspaceToolbar() {
|
||||
$this->drupalLogin($this->editor1);
|
||||
|
||||
$this->drupalPostForm('/admin/config/workflow/workspaces/add', [
|
||||
'id' => 'test_workspace',
|
||||
'label' => 'Test workspace',
|
||||
], 'Save');
|
||||
|
||||
// Activate the test workspace.
|
||||
$this->drupalPostForm('/admin/config/workflow/workspaces/manage/test_workspace/activate', [], 'Confirm');
|
||||
|
||||
$this->drupalGet('<front>');
|
||||
$page = $this->getSession()->getPage();
|
||||
// Toolbar should show the correct label.
|
||||
$this->assertTrue($page->hasLink('Test workspace'));
|
||||
|
||||
// Change the workspace label.
|
||||
$this->drupalPostForm('/admin/config/workflow/workspaces/manage/test_workspace/edit', [
|
||||
'label' => 'New name',
|
||||
], 'Save');
|
||||
|
||||
$this->drupalGet('<front>');
|
||||
$page = $this->getSession()->getPage();
|
||||
// Toolbar should show the new label.
|
||||
$this->assertTrue($page->hasLink('New name'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test changing the owner of a workspace.
|
||||
*/
|
||||
public function testWorkspaceOwner() {
|
||||
$this->drupalLogin($this->editor1);
|
||||
|
||||
$this->drupalPostForm('/admin/config/workflow/workspaces/add', [
|
||||
'id' => 'test_workspace',
|
||||
'label' => 'Test workspace',
|
||||
], 'Save');
|
||||
|
||||
$storage = \Drupal::entityTypeManager()->getStorage('workspace');
|
||||
$test_workspace = $storage->load('test_workspace');
|
||||
$this->assertEquals($this->editor1->id(), $test_workspace->getOwnerId());
|
||||
|
||||
$this->drupalPostForm('/admin/config/workflow/workspaces/manage/test_workspace/edit', [
|
||||
'uid[0][target_id]' => $this->editor2->getAccountName(),
|
||||
], 'Save');
|
||||
|
||||
$test_workspace = $storage->loadUnchanged('test_workspace');
|
||||
$this->assertEquals($this->editor2->id(), $test_workspace->getOwnerId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that editing a workspace creates a new revision.
|
||||
*/
|
||||
public function testWorkspaceFormRevisions() {
|
||||
$this->drupalLogin($this->editor1);
|
||||
$storage = \Drupal::entityTypeManager()->getStorage('workspace');
|
||||
|
||||
// The current 'stage' workspace entity should be revision 1.
|
||||
$stage_workspace = $storage->load('stage');
|
||||
$this->assertEquals('1', $stage_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());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests adding new fields to workspace entities.
|
||||
*/
|
||||
public function testWorkspaceFieldUi() {
|
||||
$user = $this->drupalCreateUser([
|
||||
'administer workspaces',
|
||||
'access administration pages',
|
||||
'administer site configuration',
|
||||
'administer workspace fields',
|
||||
'administer workspace display',
|
||||
'administer workspace form display',
|
||||
]);
|
||||
$this->drupalLogin($user);
|
||||
|
||||
$this->drupalGet('admin/config/workflow/workspaces/fields');
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
|
||||
// Create a new filed.
|
||||
$field_name = mb_strtolower($this->randomMachineName());
|
||||
$field_label = $this->randomMachineName();
|
||||
$edit = [
|
||||
'new_storage_type' => 'string',
|
||||
'label' => $field_label,
|
||||
'field_name' => $field_name,
|
||||
];
|
||||
$this->drupalPostForm("admin/config/workflow/workspaces/fields/add-field", $edit, 'Save and continue');
|
||||
$page = $this->getSession()->getPage();
|
||||
$page->pressButton('Save field settings');
|
||||
$page->pressButton('Save settings');
|
||||
|
||||
// Check that the field is displayed on the manage form display page.
|
||||
$this->drupalGet('admin/config/workflow/workspaces/form-display');
|
||||
$this->assertText($field_label);
|
||||
|
||||
// Check that the field is displayed on the manage display page.
|
||||
$this->drupalGet('admin/config/workflow/workspaces/display');
|
||||
$this->assertText($field_label);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\workspaces\Functional;
|
||||
|
||||
use Drupal\Tests\block\Traits\BlockCreationTrait;
|
||||
use Drupal\workspaces\Entity\Workspace;
|
||||
use Drupal\workspaces\WorkspaceInterface;
|
||||
|
||||
/**
|
||||
* Utility methods for use in BrowserTestBase tests.
|
||||
*
|
||||
* This trait will not work if not used in a child of BrowserTestBase.
|
||||
*/
|
||||
trait WorkspaceTestUtilities {
|
||||
|
||||
use BlockCreationTrait;
|
||||
|
||||
/**
|
||||
* Loads a single entity by its label.
|
||||
*
|
||||
* The UI approach to creating an entity doesn't make it easy to know what
|
||||
* the ID is, so this lets us make paths for an entity after it's created.
|
||||
*
|
||||
* @param string $type
|
||||
* The type of entity to load.
|
||||
* @param string $label
|
||||
* The label of the entity to load.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\EntityInterface
|
||||
* The entity.
|
||||
*/
|
||||
protected function getOneEntityByLabel($type, $label) {
|
||||
/** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */
|
||||
$entity_type_manager = \Drupal::service('entity_type.manager');
|
||||
$property = $entity_type_manager->getDefinition($type)->getKey('label');
|
||||
$entity_list = $entity_type_manager->getStorage($type)->loadByProperties([$property => $label]);
|
||||
$entity = current($entity_list);
|
||||
if (!$entity) {
|
||||
$this->fail("No {$type} entity named {$label} found.");
|
||||
}
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Workspace through the UI.
|
||||
*
|
||||
* @param string $label
|
||||
* The label of the workspace to create.
|
||||
* @param string $id
|
||||
* The ID of the workspace to create.
|
||||
* @param string $parent
|
||||
* (optional) The ID of the parent workspace. Defaults to '_none'.
|
||||
*
|
||||
* @return \Drupal\workspaces\WorkspaceInterface
|
||||
* The workspace that was just created.
|
||||
*/
|
||||
protected function createWorkspaceThroughUi($label, $id, $parent = '_none') {
|
||||
$this->drupalPostForm('/admin/config/workflow/workspaces/add', [
|
||||
'id' => $id,
|
||||
'label' => $label,
|
||||
'parent' => $parent,
|
||||
], 'Save');
|
||||
|
||||
$this->getSession()->getPage()->hasContent("$label ($id)");
|
||||
|
||||
return Workspace::load($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the workspace switcher block to the site.
|
||||
*
|
||||
* This is necessary for switchToWorkspace() to function correctly.
|
||||
*/
|
||||
protected function setupWorkspaceSwitcherBlock() {
|
||||
// Add the block to the sidebar.
|
||||
$this->placeBlock('workspace_switcher', [
|
||||
'id' => 'workspaceswitcher',
|
||||
'region' => 'sidebar_first',
|
||||
'label' => 'Workspace switcher',
|
||||
]);
|
||||
|
||||
// Confirm the block shows on the front page.
|
||||
$this->drupalGet('<front>');
|
||||
$page = $this->getSession()->getPage();
|
||||
|
||||
$this->assertTrue($page->hasContent('Workspace switcher'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a given workspace as "active" for subsequent requests.
|
||||
*
|
||||
* This assumes that the switcher block has already been setup by calling
|
||||
* setupWorkspaceSwitcherBlock().
|
||||
*
|
||||
* @param \Drupal\workspaces\WorkspaceInterface $workspace
|
||||
* The workspace to set active.
|
||||
*/
|
||||
protected function switchToWorkspace(WorkspaceInterface $workspace) {
|
||||
/** @var \Drupal\Tests\WebAssert $session */
|
||||
$session = $this->assertSession();
|
||||
$session->buttonExists('Activate');
|
||||
$this->drupalPostForm(NULL, ['workspace_id' => $workspace->id()], 'Activate');
|
||||
$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.
|
||||
*
|
||||
* @param string $label
|
||||
* The label of the Node to create.
|
||||
* @param string $bundle
|
||||
* The bundle of the Node to create.
|
||||
* @param bool $publish
|
||||
* The publishing status to set.
|
||||
*
|
||||
* @return \Drupal\node\NodeInterface
|
||||
* The Node that was just created.
|
||||
*
|
||||
* @throws \Behat\Mink\Exception\ElementNotFoundException
|
||||
*/
|
||||
protected function createNodeThroughUi($label, $bundle, $publish = TRUE) {
|
||||
$this->drupalGet('/node/add/' . $bundle);
|
||||
|
||||
/** @var \Behat\Mink\Session $session */
|
||||
$session = $this->getSession();
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
|
||||
/** @var \Behat\Mink\Element\DocumentElement $page */
|
||||
$page = $session->getPage();
|
||||
$page->fillField('Title', $label);
|
||||
if ($publish) {
|
||||
$page->findButton('Save')->click();
|
||||
}
|
||||
else {
|
||||
$page->uncheckField('Published');
|
||||
$page->findButton('Save')->click();
|
||||
}
|
||||
|
||||
$session->getPage()->hasContent("{$label} has been created");
|
||||
|
||||
return $this->getOneEntityByLabel('node', $label);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the content list has an entity's label.
|
||||
*
|
||||
* This assertion can be used to validate a particular entity exists in the
|
||||
* current workspace.
|
||||
*/
|
||||
protected function isLabelInContentOverview($label) {
|
||||
$this->drupalGet('/admin/content');
|
||||
$session = $this->getSession();
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
$page = $session->getPage();
|
||||
return $page->hasContent($label);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\workspaces\Functional;
|
||||
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
use Drupal\workspaces\Entity\Workspace;
|
||||
|
||||
/**
|
||||
* Tests permission controls on workspaces.
|
||||
*
|
||||
* @group workspaces
|
||||
*/
|
||||
class WorkspaceViewTest extends BrowserTestBase {
|
||||
|
||||
use WorkspaceTestUtilities;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = ['workspaces'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* Verifies that a user can view their own workspace.
|
||||
*/
|
||||
public function testViewOwnWorkspace() {
|
||||
$permissions = [
|
||||
'access administration pages',
|
||||
'administer site configuration',
|
||||
'create workspace',
|
||||
'edit own workspace',
|
||||
'view own workspace',
|
||||
];
|
||||
|
||||
$editor1 = $this->drupalCreateUser($permissions);
|
||||
|
||||
// Login as a limited-access user and create a workspace.
|
||||
$this->drupalLogin($editor1);
|
||||
$this->createWorkspaceThroughUi('Bears', 'bears');
|
||||
|
||||
$bears = Workspace::load('bears');
|
||||
|
||||
// Now login as a different user and create a workspace.
|
||||
$editor2 = $this->drupalCreateUser($permissions);
|
||||
|
||||
$this->drupalLogin($editor2);
|
||||
$this->createWorkspaceThroughUi('Packers', 'packers');
|
||||
|
||||
$packers = Workspace::load('packers');
|
||||
|
||||
// Load the activate form for the Bears workspace. It should fail because
|
||||
// the workspace belongs to someone else.
|
||||
$this->drupalGet("admin/config/workflow/workspaces/manage/{$bears->id()}/activate");
|
||||
$this->assertSession()->statusCodeEquals(403);
|
||||
|
||||
// But editor 2 should be able to activate the Packers workspace.
|
||||
$this->drupalGet("admin/config/workflow/workspaces/manage/{$packers->id()}/activate");
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that a user can view any workspace.
|
||||
*/
|
||||
public function testViewAnyWorkspace() {
|
||||
$permissions = [
|
||||
'access administration pages',
|
||||
'administer site configuration',
|
||||
'create workspace',
|
||||
'edit own workspace',
|
||||
'view any workspace',
|
||||
];
|
||||
|
||||
$editor1 = $this->drupalCreateUser($permissions);
|
||||
|
||||
// Login as a limited-access user and create a workspace.
|
||||
$this->drupalLogin($editor1);
|
||||
|
||||
$this->createWorkspaceThroughUi('Bears', 'bears');
|
||||
|
||||
$bears = Workspace::load('bears');
|
||||
|
||||
// Now login as a different user and create a workspace.
|
||||
$editor2 = $this->drupalCreateUser($permissions);
|
||||
|
||||
$this->drupalLogin($editor2);
|
||||
$this->createWorkspaceThroughUi('Packers', 'packers');
|
||||
|
||||
$packers = Workspace::load('packers');
|
||||
|
||||
// Load the activate form for the Bears workspace. This user should be
|
||||
// able to see both workspaces because of the "view any" permission.
|
||||
$this->drupalGet("admin/config/workflow/workspaces/manage/{$bears->id()}/activate");
|
||||
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
|
||||
// But editor 2 should be able to activate the Packers workspace.
|
||||
$this->drupalGet("admin/config/workflow/workspaces/manage/{$packers->id()}/activate");
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\workspaces\Functional;
|
||||
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
|
||||
/**
|
||||
* Tests uninstalling the Workspaces module.
|
||||
*
|
||||
* @group workspaces
|
||||
*/
|
||||
class WorkspacesUninstallTest extends BrowserTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $profile = 'standard';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = ['workspaces'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* Tests deleting workspace entities and uninstalling Workspaces module.
|
||||
*/
|
||||
public function testUninstallingWorkspace() {
|
||||
$this->drupalLogin($this->rootUser);
|
||||
$this->drupalGet('/admin/modules/uninstall');
|
||||
$session = $this->assertSession();
|
||||
$session->linkExists('Remove workspaces');
|
||||
$this->clickLink('Remove workspaces');
|
||||
$session->pageTextContains('Are you sure you want to delete all workspaces?');
|
||||
$this->drupalPostForm('/admin/modules/uninstall/entity/workspace', [], 'Delete all workspaces');
|
||||
$this->drupalPostForm('admin/modules/uninstall', ['uninstall[workspaces]' => TRUE], 'Uninstall');
|
||||
$this->drupalPostForm(NULL, [], 'Uninstall');
|
||||
$session->pageTextContains('The selected modules have been uninstalled.');
|
||||
$session->pageTextNotContains('Workspaces');
|
||||
|
||||
$this->assertFalse(\Drupal::database()->schema()->fieldExists('node_revision', 'workspace'));
|
||||
|
||||
// Verify that the revision metadata key has been removed.
|
||||
$entity_type = \Drupal::entityDefinitionUpdateManager()->getEntityType('node');
|
||||
$revision_metadata_keys = $entity_type->get('revision_metadata_keys');
|
||||
$this->assertArrayNotHasKey('workspace', $revision_metadata_keys);
|
||||
$required_revision_metadata_keys = $entity_type->get('requiredRevisionMetadataKeys');
|
||||
$this->assertArrayNotHasKey('workspace', $required_revision_metadata_keys);
|
||||
}
|
||||
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\workspaces\FunctionalJavascript;
|
||||
|
||||
use Drupal\Tests\system\FunctionalJavascript\OffCanvasTestBase;
|
||||
|
||||
/**
|
||||
* Tests workspace settings stray integration.
|
||||
*
|
||||
* @group workspaces
|
||||
*/
|
||||
class WorkspaceToolbarIntegrationTest extends OffCanvasTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = ['toolbar', 'workspaces'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $defaultTheme = 'stark';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
$admin_user = $this->drupalCreateUser([
|
||||
'administer workspaces',
|
||||
'access toolbar',
|
||||
'access administration pages',
|
||||
]);
|
||||
$this->drupalLogin($admin_user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test workspace canvas can be toggled with JavaScript.
|
||||
*/
|
||||
public function testWorkspaceCanvasToggling() {
|
||||
$page = $this->getSession()->getPage();
|
||||
$assert_session = $this->assertSession();
|
||||
|
||||
// Set size for horizontal toolbar.
|
||||
$this->getSession()->resizeWindow(1200, 600);
|
||||
$this->drupalGet('<front>');
|
||||
// Wait for toolbar to appear.
|
||||
$this->assertNotEmpty($assert_session->waitForElement('css', 'body.toolbar-horizontal'));
|
||||
|
||||
// Open workspace canvas.
|
||||
$page->clickLink('Switch workspace');
|
||||
$this->waitForOffCanvasToOpen('top');
|
||||
$assert_session->elementExists('css', '.workspaces-dialog');
|
||||
|
||||
// Close Canvas.
|
||||
$page->pressButton('Close');
|
||||
$this->waitForOffCanvasToClose();
|
||||
$assert_session->assertNoElementAfterWait('css', '.workspaces-dialog');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test workspace switch and landing page behavior.
|
||||
*/
|
||||
public function testWorkspaceSwitch() {
|
||||
$page = $this->getSession()->getPage();
|
||||
$assert_session = $this->assertSession();
|
||||
|
||||
// Wait for toolbar to appear.
|
||||
$this->getSession()->resizeWindow(1200, 600);
|
||||
$this->drupalGet('admin');
|
||||
|
||||
// Wait for toolbar to appear.
|
||||
$this->assertNotEmpty($assert_session->waitForElement('css', 'body.toolbar-horizontal'));
|
||||
|
||||
// Open workspace canvas.
|
||||
$page->clickLink('Switch workspace');
|
||||
$this->waitForOffCanvasToOpen('top');
|
||||
|
||||
// Click 'stage' workspace and confirm switch.
|
||||
$page->clickLink('Stage');
|
||||
$this->assertElementVisibleAfterWait('css', '.workspace-activate-form.workspace-confirm-form');
|
||||
$page->find('css', '.ui-dialog-buttonset .button--primary')->click();
|
||||
$assert_session->waitForElementVisible('css', '.messages--status');
|
||||
|
||||
// Make sure we stay on same page after switch.
|
||||
$assert_session->responseContains('<em class="placeholder">Stage</em> is now the active workspace.');
|
||||
$assert_session->addressEquals('admin');
|
||||
}
|
||||
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\workspaces\Kernel;
|
||||
|
||||
use Drupal\Core\Field\BaseFieldDefinition;
|
||||
use Drupal\entity_test\Entity\EntityTest;
|
||||
use Drupal\entity_test\Entity\EntityTestMulRevPub;
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
use Drupal\Tests\user\Traits\UserCreationTrait;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\workspaces\Plugin\Validation\Constraint\EntityReferenceSupportedNewEntitiesConstraintValidator
|
||||
* @group workspaces
|
||||
*/
|
||||
class EntityReferenceSupportedNewEntitiesConstraintValidatorTest extends KernelTestBase {
|
||||
|
||||
use UserCreationTrait;
|
||||
use WorkspaceTestTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'system',
|
||||
'user',
|
||||
'workspaces',
|
||||
'entity_test',
|
||||
'path_alias',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->installEntitySchema('user');
|
||||
$this->installSchema('system', ['sequences']);
|
||||
$this->createUser();
|
||||
|
||||
$fields['supported_reference'] = BaseFieldDefinition::create('entity_reference')->setSetting('target_type', 'entity_test_mulrevpub');
|
||||
$fields['unsupported_reference'] = BaseFieldDefinition::create('entity_reference')->setSetting('target_type', 'entity_test');
|
||||
$this->container->get('state')->set('entity_test_mulrevpub.additional_base_field_definitions', $fields);
|
||||
|
||||
$this->installEntitySchema('entity_test_mulrevpub');
|
||||
$this->initializeWorkspacesModule();
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::validate
|
||||
*/
|
||||
public function testNewEntitiesAllowedInDefaultWorkspace() {
|
||||
$entity = EntityTestMulRevPub::create([
|
||||
'unsupported_reference' => [
|
||||
'entity' => EntityTest::create([]),
|
||||
],
|
||||
'supported_reference' => [
|
||||
'entity' => EntityTest::create([]),
|
||||
],
|
||||
]);
|
||||
$this->assertCount(0, $entity->validate());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::validate
|
||||
*/
|
||||
public function testNewEntitiesForbiddenInNonDefaultWorkspace() {
|
||||
$this->switchToWorkspace('stage');
|
||||
$entity = EntityTestMulRevPub::create([
|
||||
'unsupported_reference' => [
|
||||
'entity' => EntityTest::create([]),
|
||||
],
|
||||
'supported_reference' => [
|
||||
'entity' => EntityTestMulRevPub::create([]),
|
||||
],
|
||||
]);
|
||||
$violations = $entity->validate();
|
||||
$this->assertCount(1, $violations);
|
||||
$this->assertEquals('<em class="placeholder">Test entity entities</em> can only be created in the default workspace.', $violations[0]->getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\workspaces\Kernel;
|
||||
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
use Drupal\Tests\user\Traits\UserCreationTrait;
|
||||
use Drupal\workspaces\Entity\Workspace;
|
||||
use Drupal\workspaces\WorkspaceAccessException;
|
||||
|
||||
/**
|
||||
* Tests access on workspaces.
|
||||
*
|
||||
* @group workspaces
|
||||
*/
|
||||
class WorkspaceAccessTest extends KernelTestBase {
|
||||
|
||||
use UserCreationTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'user',
|
||||
'system',
|
||||
'workspaces',
|
||||
'workspace_access_test',
|
||||
'path_alias',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->installSchema('system', ['sequences']);
|
||||
$this->installSchema('workspaces', ['workspace_association']);
|
||||
|
||||
$this->installEntitySchema('workspace');
|
||||
$this->installEntitySchema('user');
|
||||
|
||||
// User 1.
|
||||
$this->createUser();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test cases for testWorkspaceAccess().
|
||||
*
|
||||
* @return array
|
||||
* An array of operations and permissions to test with.
|
||||
*/
|
||||
public function operationCases() {
|
||||
return [
|
||||
['create', 'create workspace'],
|
||||
['view', 'view any workspace'],
|
||||
['view', 'view own workspace'],
|
||||
['update', 'edit any workspace'],
|
||||
['update', 'edit own workspace'],
|
||||
['delete', 'delete any workspace'],
|
||||
['delete', 'delete own workspace'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies all workspace roles have the correct access for the operation.
|
||||
*
|
||||
* @param string $operation
|
||||
* The operation to test with.
|
||||
* @param string $permission
|
||||
* The permission to test with.
|
||||
*
|
||||
* @dataProvider operationCases
|
||||
*/
|
||||
public function testWorkspaceAccess($operation, $permission) {
|
||||
$user = $this->createUser();
|
||||
$this->setCurrentUser($user);
|
||||
$workspace = Workspace::create(['id' => 'oak']);
|
||||
$workspace->save();
|
||||
|
||||
$this->assertFalse($workspace->access($operation, $user));
|
||||
|
||||
\Drupal::entityTypeManager()->getAccessControlHandler('workspace')->resetCache();
|
||||
$role = $this->createRole([$permission]);
|
||||
$user->addRole($role);
|
||||
$this->assertTrue($workspace->access($operation, $user));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests workspace publishing access.
|
||||
*/
|
||||
public function testPublishWorkspaceAccess() {
|
||||
$user = $this->createUser([
|
||||
'view own workspace',
|
||||
'edit own workspace',
|
||||
]);
|
||||
$this->setCurrentUser($user);
|
||||
|
||||
$workspace = Workspace::create(['id' => 'stage']);
|
||||
$workspace->save();
|
||||
|
||||
// Check that, by default, an admin user is allowed to publish a workspace.
|
||||
$workspace->publish();
|
||||
|
||||
// Simulate an external factor which decides that a workspace can not be
|
||||
// published.
|
||||
\Drupal::state()->set('workspace_access_test.result.publish', AccessResult::forbidden());
|
||||
\Drupal::entityTypeManager()->getAccessControlHandler('workspace')->resetCache();
|
||||
|
||||
$this->expectException(WorkspaceAccessException::class);
|
||||
$workspace->publish();
|
||||
}
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\workspaces\Plugin\EntityReferenceSelection\WorkspaceSelection
|
||||
*/
|
||||
public function testWorkspaceSelection() {
|
||||
$own_permission_user = $this->createUser(['view own workspace']);
|
||||
$any_permission_user = $this->createUser(['view any workspace']);
|
||||
$admin_permission_user = $this->createUser(['administer workspaces']);
|
||||
|
||||
// Create the following workspace hierarchy:
|
||||
// - top1 ($own_permission_user)
|
||||
// --- child1_1 ($own_permission_user)
|
||||
// --- child1_2 ($any_permission_user)
|
||||
// ----- child1_2_1 ($any_permission_user)
|
||||
// - top2 ($admin_permission_user)
|
||||
// --- child2_1 ($admin_permission_user)
|
||||
$created_time = \Drupal::time()->getCurrentTime();
|
||||
Workspace::create([
|
||||
'uid' => $own_permission_user->id(),
|
||||
'id' => 'top1',
|
||||
'label' => 'top1',
|
||||
'created' => ++$created_time,
|
||||
])->save();
|
||||
Workspace::create([
|
||||
'uid' => $own_permission_user->id(),
|
||||
'id' => 'child1_1',
|
||||
'parent' => 'top1',
|
||||
'label' => 'child1_1',
|
||||
'created' => ++$created_time,
|
||||
])->save();
|
||||
Workspace::create([
|
||||
'uid' => $any_permission_user->id(),
|
||||
'id' => 'child1_2',
|
||||
'parent' => 'top1',
|
||||
'label' => 'child1_2',
|
||||
'created' => ++$created_time,
|
||||
])->save();
|
||||
Workspace::create([
|
||||
'uid' => $any_permission_user->id(),
|
||||
'id' => 'child1_2_1',
|
||||
'parent' => 'child1_2',
|
||||
'label' => 'child1_2_1',
|
||||
'created' => ++$created_time,
|
||||
])->save();
|
||||
Workspace::create([
|
||||
'uid' => $admin_permission_user->id(),
|
||||
'id' => 'top2',
|
||||
'label' => 'top2',
|
||||
'created' => ++$created_time,
|
||||
])->save();
|
||||
Workspace::create([
|
||||
'uid' => $admin_permission_user->id(),
|
||||
'id' => 'child2_1',
|
||||
'parent' => 'top2',
|
||||
'label' => 'child2_1',
|
||||
'created' => ++$created_time,
|
||||
])->save();
|
||||
|
||||
/** @var \Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface $selection_handler */
|
||||
$selection_handler = \Drupal::service('plugin.manager.entity_reference_selection')->getInstance([
|
||||
'target_type' => 'workspace',
|
||||
'handler' => 'default',
|
||||
'sort' => [
|
||||
'field' => 'created',
|
||||
'direction' => 'asc',
|
||||
],
|
||||
]);
|
||||
|
||||
// The $own_permission_user should only be allowed to reference 'top1' and
|
||||
// 'child1_1'.
|
||||
$this->setCurrentUser($own_permission_user);
|
||||
$expected = [
|
||||
'top1',
|
||||
'child1_1',
|
||||
];
|
||||
$this->assertEquals($expected, array_keys($selection_handler->getReferenceableEntities()['workspace']));
|
||||
$this->assertEquals($expected, array_keys($selection_handler->getReferenceableEntities(NULL, 'CONTAINS', 3)['workspace']));
|
||||
$expected = [
|
||||
'top1',
|
||||
];
|
||||
$this->assertEquals($expected, array_keys($selection_handler->getReferenceableEntities('top')['workspace']));
|
||||
|
||||
// The $any_permission_user and $admin_permission_user should be allowed to
|
||||
// reference any workspace.
|
||||
$expected_all = [
|
||||
'top1',
|
||||
'child1_1',
|
||||
'child1_2',
|
||||
'child1_2_1',
|
||||
'top2',
|
||||
'child2_1',
|
||||
];
|
||||
$expected_3 = [
|
||||
'top1',
|
||||
'child1_1',
|
||||
'child1_2',
|
||||
];
|
||||
$expected_top = [
|
||||
'top1',
|
||||
'top2',
|
||||
];
|
||||
$this->setCurrentUser($any_permission_user);
|
||||
$this->assertEquals($expected_all, array_keys($selection_handler->getReferenceableEntities()['workspace']));
|
||||
$this->assertEquals($expected_3, array_keys($selection_handler->getReferenceableEntities(NULL, 'CONTAINS', 3)['workspace']));
|
||||
$this->assertEquals($expected_top, array_keys($selection_handler->getReferenceableEntities('top')['workspace']));
|
||||
|
||||
$this->setCurrentUser($admin_permission_user);
|
||||
$this->assertEquals($expected_all, array_keys($selection_handler->getReferenceableEntities()['workspace']));
|
||||
$this->assertEquals($expected_3, array_keys($selection_handler->getReferenceableEntities(NULL, 'CONTAINS', 3)['workspace']));
|
||||
$this->assertEquals($expected_top, array_keys($selection_handler->getReferenceableEntities('top')['workspace']));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\workspaces\Kernel;
|
||||
|
||||
use Drupal\Core\Entity\EntityStorageException;
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
|
||||
use Drupal\Tests\node\Traits\NodeCreationTrait;
|
||||
use Drupal\Tests\user\Traits\UserCreationTrait;
|
||||
use Drupal\workspaces\Entity\Workspace;
|
||||
|
||||
/**
|
||||
* Tests CRUD operations for workspaces.
|
||||
*
|
||||
* @group workspaces
|
||||
*/
|
||||
class WorkspaceCRUDTest extends KernelTestBase {
|
||||
|
||||
use UserCreationTrait;
|
||||
use NodeCreationTrait;
|
||||
use ContentTypeCreationTrait;
|
||||
use WorkspaceTestTrait;
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The state service.
|
||||
*
|
||||
* @var \Drupal\Core\State\StateInterface
|
||||
*/
|
||||
protected $state;
|
||||
|
||||
/**
|
||||
* The workspace replication manager.
|
||||
*
|
||||
* @var \Drupal\workspaces\WorkspaceManagerInterface
|
||||
*/
|
||||
protected $workspaceManager;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = [
|
||||
'user',
|
||||
'system',
|
||||
'workspaces',
|
||||
'field',
|
||||
'filter',
|
||||
'node',
|
||||
'text',
|
||||
'path_alias',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->setUpCurrentUser();
|
||||
|
||||
$this->installSchema('system', ['key_value_expire']);
|
||||
$this->installSchema('node', ['node_access']);
|
||||
|
||||
$this->installEntitySchema('workspace');
|
||||
$this->installSchema('workspaces', ['workspace_association']);
|
||||
$this->installEntitySchema('node');
|
||||
|
||||
$this->installConfig(['filter', 'node', 'system']);
|
||||
|
||||
$this->createContentType(['type' => 'page']);
|
||||
|
||||
$this->entityTypeManager = \Drupal::entityTypeManager();
|
||||
$this->state = \Drupal::state();
|
||||
$this->workspaceManager = \Drupal::service('workspaces.manager');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the deletion of workspaces.
|
||||
*/
|
||||
public function testDeletingWorkspaces() {
|
||||
$admin = $this->createUser([
|
||||
'administer nodes',
|
||||
'create workspace',
|
||||
'view any workspace',
|
||||
'edit any workspace',
|
||||
'delete any workspace',
|
||||
]);
|
||||
$this->setCurrentUser($admin);
|
||||
|
||||
/** @var \Drupal\workspaces\WorkspaceAssociationInterface $workspace_association */
|
||||
$workspace_association = \Drupal::service('workspaces.association');
|
||||
|
||||
// Create a workspace with a very small number of associated node revisions.
|
||||
$workspace_1 = Workspace::create([
|
||||
'id' => 'gibbon',
|
||||
'label' => 'Gibbon',
|
||||
]);
|
||||
$workspace_1->save();
|
||||
$this->workspaceManager->setActiveWorkspace($workspace_1);
|
||||
|
||||
$workspace_1_node_1 = $this->createNode(['status' => FALSE]);
|
||||
$workspace_1_node_2 = $this->createNode(['status' => FALSE]);
|
||||
|
||||
// The 'live' workspace should have 2 revisions now. The initial revision
|
||||
// for each node.
|
||||
$live_revisions = $this->getUnassociatedRevisions('node');
|
||||
$this->assertCount(2, $live_revisions);
|
||||
|
||||
for ($i = 0; $i < 4; $i++) {
|
||||
$workspace_1_node_1->setNewRevision(TRUE);
|
||||
$workspace_1_node_1->save();
|
||||
|
||||
$workspace_1_node_2->setNewRevision(TRUE);
|
||||
$workspace_1_node_2->save();
|
||||
}
|
||||
|
||||
// The workspace should now track 2 nodes.
|
||||
$tracked_entities = $workspace_association->getTrackedEntities($workspace_1->id());
|
||||
$this->assertCount(2, $tracked_entities['node']);
|
||||
|
||||
// There should still be 2 revisions associated with 'live'.
|
||||
$live_revisions = $this->getUnassociatedRevisions('node');
|
||||
$this->assertCount(2, $live_revisions);
|
||||
|
||||
// The other 8 revisions should be associated with 'workspace_1'.
|
||||
$associated_revisions = $workspace_association->getAssociatedRevisions($workspace_1->id(), 'node');
|
||||
$this->assertCount(8, $associated_revisions);
|
||||
|
||||
// Check that we are allowed to delete the workspace.
|
||||
$this->assertTrue($workspace_1->access('delete', $admin));
|
||||
|
||||
// Delete the workspace and check that all the workspace_association
|
||||
// entities and all the node revisions have been deleted as well.
|
||||
$workspace_1->delete();
|
||||
|
||||
// There are no more tracked entities in 'workspace_1'.
|
||||
$tracked_entities = $workspace_association->getTrackedEntities($workspace_1->id());
|
||||
$this->assertEmpty($tracked_entities);
|
||||
|
||||
// There are no more revisions associated with 'workspace_1'.
|
||||
$associated_revisions = $workspace_association->getAssociatedRevisions($workspace_1->id(), 'node');
|
||||
$this->assertCount(0, $associated_revisions);
|
||||
|
||||
// There should still be 2 revisions associated with 'live'.
|
||||
$live_revisions = $this->getUnassociatedRevisions('node');
|
||||
$this->assertCount(2, $live_revisions);
|
||||
|
||||
// Create another workspace, this time with a larger number of associated
|
||||
// node revisions so we can test the batch purge process.
|
||||
$workspace_2 = Workspace::create([
|
||||
'id' => 'baboon',
|
||||
'label' => 'Baboon',
|
||||
]);
|
||||
$workspace_2->save();
|
||||
$this->workspaceManager->setActiveWorkspace($workspace_2);
|
||||
|
||||
$workspace_2_node_1 = $this->createNode(['status' => FALSE]);
|
||||
for ($i = 0; $i < 59; $i++) {
|
||||
$workspace_2_node_1->setNewRevision(TRUE);
|
||||
$workspace_2_node_1->save();
|
||||
}
|
||||
|
||||
// Now there is one entity tracked in 'workspace_2'.
|
||||
$tracked_entities = $workspace_association->getTrackedEntities($workspace_2->id());
|
||||
$this->assertCount(1, $tracked_entities['node']);
|
||||
|
||||
// One revision of this entity is in 'live'.
|
||||
$live_revisions = $this->getUnassociatedRevisions('node', [$workspace_2_node_1->id()]);
|
||||
$this->assertCount(1, $live_revisions);
|
||||
|
||||
// The other 59 are associated with 'workspace_2'.
|
||||
$associated_revisions = $workspace_association->getAssociatedRevisions($workspace_2->id(), 'node', [$workspace_2_node_1->id()]);
|
||||
$this->assertCount(59, $associated_revisions);
|
||||
|
||||
// Delete the workspace and check that we still have 9 revision left to
|
||||
// delete.
|
||||
$workspace_2->delete();
|
||||
$associated_revisions = $workspace_association->getAssociatedRevisions($workspace_2->id(), 'node', [$workspace_2_node_1->id()]);
|
||||
$this->assertCount(9, $associated_revisions);
|
||||
|
||||
// The live revision is also still there.
|
||||
$live_revisions = $this->getUnassociatedRevisions('node', [$workspace_2_node_1->id()]);
|
||||
$this->assertCount(1, $live_revisions);
|
||||
|
||||
$workspace_deleted = \Drupal::state()->get('workspace.deleted');
|
||||
$this->assertCount(1, $workspace_deleted);
|
||||
|
||||
// Check that we can not create another workspace with the same ID while its
|
||||
// data purging is not finished.
|
||||
$workspace_3 = Workspace::create([
|
||||
'id' => 'baboon',
|
||||
'label' => 'Baboon',
|
||||
]);
|
||||
$violations = $workspace_3->validate();
|
||||
$this->assertCount(1, $violations);
|
||||
$this->assertEquals('A workspace with this ID has been deleted but data still exists for it.', $violations[0]->getMessage());
|
||||
|
||||
// Running cron should delete the remaining data as well as the workspace ID
|
||||
// from the "workspace.delete" state entry.
|
||||
\Drupal::service('cron')->run();
|
||||
|
||||
$associated_revisions = $workspace_association->getTrackedEntities($workspace_2->id());
|
||||
$this->assertCount(0, $associated_revisions);
|
||||
|
||||
// 'workspace_2 'is empty now.
|
||||
$associated_revisions = $workspace_association->getAssociatedRevisions($workspace_2->id(), 'node', [$workspace_2_node_1->id()]);
|
||||
$this->assertCount(0, $associated_revisions);
|
||||
$tracked_entities = $workspace_association->getTrackedEntities($workspace_2->id());
|
||||
$this->assertEmpty($tracked_entities);
|
||||
|
||||
// The 3 revisions in 'live' remain.
|
||||
$live_revisions = $this->getUnassociatedRevisions('node');
|
||||
$this->assertCount(3, $live_revisions);
|
||||
|
||||
$workspace_deleted = \Drupal::state()->get('workspace.deleted');
|
||||
$this->assertCount(0, $workspace_deleted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that deleting a workspace keeps its already published content.
|
||||
*/
|
||||
public function testDeletingPublishedWorkspace() {
|
||||
$admin = $this->createUser([
|
||||
'administer nodes',
|
||||
'create workspace',
|
||||
'view own workspace',
|
||||
'edit own workspace',
|
||||
'delete own workspace',
|
||||
]);
|
||||
$this->setCurrentUser($admin);
|
||||
|
||||
$live_workspace = Workspace::create([
|
||||
'id' => 'live',
|
||||
'label' => 'Live',
|
||||
]);
|
||||
$live_workspace->save();
|
||||
$workspace = Workspace::create([
|
||||
'id' => 'stage',
|
||||
'label' => 'Stage',
|
||||
]);
|
||||
$workspace->save();
|
||||
$this->workspaceManager->setActiveWorkspace($workspace);
|
||||
|
||||
// Create a new node in the 'stage' workspace
|
||||
$node = $this->createNode(['status' => TRUE]);
|
||||
|
||||
// Create an additional workspace-specific revision for the node.
|
||||
$node->setNewRevision(TRUE);
|
||||
$node->save();
|
||||
|
||||
// The node should have 3 revisions now: a default and 2 pending ones.
|
||||
$revisions = $this->entityTypeManager->getStorage('node')->loadMultipleRevisions([1, 2, 3]);
|
||||
$this->assertCount(3, $revisions);
|
||||
$this->assertTrue($revisions[1]->isDefaultRevision());
|
||||
$this->assertFalse($revisions[2]->isDefaultRevision());
|
||||
$this->assertFalse($revisions[3]->isDefaultRevision());
|
||||
|
||||
// Publish the workspace, which should mark revision 3 as the default one
|
||||
// and keep revision 2 as a 'source' draft revision.
|
||||
$workspace->publish();
|
||||
$revisions = $this->entityTypeManager->getStorage('node')->loadMultipleRevisions([1, 2, 3]);
|
||||
$this->assertFalse($revisions[1]->isDefaultRevision());
|
||||
$this->assertFalse($revisions[2]->isDefaultRevision());
|
||||
$this->assertTrue($revisions[3]->isDefaultRevision());
|
||||
|
||||
// Create two new workspace-revisions for the node.
|
||||
$node->setNewRevision(TRUE);
|
||||
$node->save();
|
||||
$node->setNewRevision(TRUE);
|
||||
$node->save();
|
||||
|
||||
// The node should now have 5 revisions.
|
||||
$revisions = $this->entityTypeManager->getStorage('node')->loadMultipleRevisions([1, 2, 3, 4, 5]);
|
||||
$this->assertFalse($revisions[1]->isDefaultRevision());
|
||||
$this->assertFalse($revisions[2]->isDefaultRevision());
|
||||
$this->assertTrue($revisions[3]->isDefaultRevision());
|
||||
$this->assertFalse($revisions[4]->isDefaultRevision());
|
||||
$this->assertFalse($revisions[5]->isDefaultRevision());
|
||||
|
||||
// Delete the workspace and check that only the two new pending revisions
|
||||
// were deleted by the workspace purging process.
|
||||
$workspace->delete();
|
||||
|
||||
$revisions = $this->entityTypeManager->getStorage('node')->loadMultipleRevisions([1, 2, 3, 4, 5]);
|
||||
$this->assertCount(3, $revisions);
|
||||
$this->assertFalse($revisions[1]->isDefaultRevision());
|
||||
$this->assertFalse($revisions[2]->isDefaultRevision());
|
||||
$this->assertTrue($revisions[3]->isDefaultRevision());
|
||||
$this->assertFalse(isset($revisions[4]));
|
||||
$this->assertFalse(isset($revisions[5]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that a workspace with children can not be deleted.
|
||||
*/
|
||||
public function testDeletingWorkspaceWithChildren() {
|
||||
$stage = Workspace::create(['id' => 'stage', 'label' => 'Stage']);
|
||||
$stage->save();
|
||||
|
||||
$dev = Workspace::create(['id' => 'dev', 'label' => 'Dev', 'parent' => 'stage']);
|
||||
$dev->save();
|
||||
|
||||
// Check that a workspace which has children can not be deleted.
|
||||
try {
|
||||
$stage->delete();
|
||||
$this->fail('The Stage workspace has children and should not be deletable.');
|
||||
}
|
||||
catch (EntityStorageException $e) {
|
||||
$this->assertEquals('The Stage workspace can not be deleted because it has child workspaces.', $e->getMessage());
|
||||
$this->assertNotNull(Workspace::load('stage'));
|
||||
}
|
||||
|
||||
// Check that if we delete its child first, the parent workspace can also be
|
||||
// deleted.
|
||||
$dev->delete();
|
||||
$stage->delete();
|
||||
$this->assertNull(Workspace::load('dev'));
|
||||
$this->assertNull(Workspace::load('stage'));
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,212 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\workspaces\Kernel;
|
||||
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
|
||||
use Drupal\Tests\node\Traits\NodeCreationTrait;
|
||||
use Drupal\Tests\user\Traits\UserCreationTrait;
|
||||
|
||||
/**
|
||||
* Tests workspace merging.
|
||||
*
|
||||
* @coversDefaultClass \Drupal\workspaces\WorkspaceMerger
|
||||
*
|
||||
* @group workspaces
|
||||
*/
|
||||
class WorkspaceMergerTest extends KernelTestBase {
|
||||
|
||||
use ContentTypeCreationTrait;
|
||||
use NodeCreationTrait;
|
||||
use UserCreationTrait;
|
||||
use WorkspaceTestTrait;
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* An array of nodes created before installing the Workspaces module.
|
||||
*
|
||||
* @var \Drupal\node\NodeInterface[]
|
||||
*/
|
||||
protected $nodes = [];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'field',
|
||||
'filter',
|
||||
'node',
|
||||
'text',
|
||||
'user',
|
||||
'system',
|
||||
'path_alias',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->entityTypeManager = \Drupal::entityTypeManager();
|
||||
|
||||
$this->installEntitySchema('node');
|
||||
$this->installEntitySchema('user');
|
||||
|
||||
$this->installConfig(['filter', 'node', 'system']);
|
||||
|
||||
$this->installSchema('system', ['key_value_expire', 'sequences']);
|
||||
$this->installSchema('node', ['node_access']);
|
||||
|
||||
$this->createContentType(['type' => 'article']);
|
||||
|
||||
$this->setCurrentUser($this->createUser(['administer nodes']));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests workspace merging.
|
||||
*
|
||||
* @covers ::merge
|
||||
* @covers ::getNumberOfChangesOnSource
|
||||
* @covers ::getNumberOfChangesOnTarget
|
||||
* @covers ::getDifferringRevisionIdsOnSource
|
||||
* @covers ::getDifferringRevisionIdsOnTarget
|
||||
*/
|
||||
public function testWorkspaceMerger() {
|
||||
$this->initializeWorkspacesModule();
|
||||
$this->createWorkspaceHierarchy();
|
||||
|
||||
// Generate content in the workspace hierarchy with the following structure:
|
||||
// Live:
|
||||
// - Test article 1 - live
|
||||
//
|
||||
// Stage:
|
||||
// - Test article 2 - stage
|
||||
//
|
||||
// Dev:
|
||||
// - Test article 2 - stage
|
||||
// - Test article 3 - dev
|
||||
//
|
||||
// Local 1:
|
||||
// - Test article 2 - stage
|
||||
// - Test article 3 - dev
|
||||
// - Test article 4 - local_1
|
||||
//
|
||||
// Local 2:
|
||||
// - Test article 2 - stage
|
||||
// - Test article 3 - dev
|
||||
//
|
||||
// Note that the contents of each workspace are inherited automatically in
|
||||
// each of its descendants.
|
||||
$this->createNode(['title' => 'Test article 1 - live', 'type' => 'article']);
|
||||
|
||||
// This creates revisions 2 and 3. Revision 2 is an unpublished default
|
||||
// revision (which is also available in Live), and revision 3 is a published
|
||||
// pending revision that is available in Stage and all its descendants.
|
||||
$this->switchToWorkspace('stage');
|
||||
$this->createNode(['title' => 'Test article 2 - stage', 'type' => 'article']);
|
||||
|
||||
$expected_workspace_association = [
|
||||
'stage' => [3],
|
||||
'dev' => [3],
|
||||
'local_1' => [3],
|
||||
'local_2' => [3],
|
||||
'qa' => [],
|
||||
];
|
||||
$this->assertWorkspaceAssociation($expected_workspace_association, 'node');
|
||||
|
||||
// Create the second test article in Dev. This creates revisions 4 and 5.
|
||||
// Revision 4 is default and unpublished, and revision 5 is now being
|
||||
// tracked in Dev and its descendants.
|
||||
$this->switchToWorkspace('dev');
|
||||
$this->createNode(['title' => 'Test article 3 - dev', 'type' => 'article']);
|
||||
|
||||
$expected_workspace_association = [
|
||||
'stage' => [3],
|
||||
'dev' => [3, 5],
|
||||
'local_1' => [3, 5],
|
||||
'local_2' => [3, 5],
|
||||
'qa' => [],
|
||||
];
|
||||
$this->assertWorkspaceAssociation($expected_workspace_association, 'node');
|
||||
|
||||
// Create the third article in Local 1. This creates revisions 6 and 7.
|
||||
// Revision 6 is default and unpublished, and revision 7 is now being
|
||||
// tracked in the Local 1.
|
||||
$this->switchToWorkspace('local_1');
|
||||
$this->createNode(['title' => 'Test article 4 - local_1', 'type' => 'article']);
|
||||
|
||||
$expected_workspace_association = [
|
||||
'stage' => [3],
|
||||
'dev' => [3, 5],
|
||||
'local_1' => [3, 5, 7],
|
||||
'local_2' => [3, 5],
|
||||
'qa' => [],
|
||||
];
|
||||
$this->assertWorkspaceAssociation($expected_workspace_association, 'node');
|
||||
|
||||
/** @var \Drupal\workspaces\WorkspaceMergerInterface $workspace_merger */
|
||||
$workspace_merger = \Drupal::service('workspaces.operation_factory')->getMerger($this->workspaces['local_1'], $this->workspaces['dev']);
|
||||
|
||||
// Check that there is no content in Dev that's not also in Local 1.
|
||||
$this->assertEmpty($workspace_merger->getDifferringRevisionIdsOnTarget());
|
||||
$this->assertEquals(0, $workspace_merger->getNumberOfChangesOnTarget());
|
||||
|
||||
// Check that there is only one node in Local 1 that's not available in Dev,
|
||||
// revision 7 created above for the fourth test article.
|
||||
$expected = [
|
||||
'node' => [7 => 4],
|
||||
];
|
||||
$this->assertEquals($expected, $workspace_merger->getDifferringRevisionIdsOnSource());
|
||||
$this->assertEquals(1, $workspace_merger->getNumberOfChangesOnSource());
|
||||
|
||||
// Merge the contents of Local 1 into Dev, and check that Dev, Local 1 and
|
||||
// Local 2 have the same content.
|
||||
$workspace_merger->merge();
|
||||
|
||||
$this->assertEmpty($workspace_merger->getDifferringRevisionIdsOnTarget());
|
||||
$this->assertEquals(0, $workspace_merger->getNumberOfChangesOnTarget());
|
||||
$this->assertEmpty($workspace_merger->getDifferringRevisionIdsOnSource());
|
||||
$this->assertEquals(0, $workspace_merger->getNumberOfChangesOnSource());
|
||||
|
||||
$this->switchToWorkspace('dev');
|
||||
$expected_workspace_association = [
|
||||
'stage' => [3],
|
||||
'dev' => [3, 5, 7],
|
||||
'local_1' => [3, 5, 7],
|
||||
'local_2' => [3, 5, 7],
|
||||
'qa' => [],
|
||||
];
|
||||
$this->assertWorkspaceAssociation($expected_workspace_association, 'node');
|
||||
|
||||
$workspace_merger = \Drupal::service('workspaces.operation_factory')->getMerger($this->workspaces['local_1'], $this->workspaces['stage']);
|
||||
|
||||
// Check that there is no content in Stage that's not also in Local 1.
|
||||
$this->assertEmpty($workspace_merger->getDifferringRevisionIdsOnTarget());
|
||||
$this->assertEquals(0, $workspace_merger->getNumberOfChangesOnTarget());
|
||||
|
||||
// Check that the difference between Local 1 and Stage are the two revisions
|
||||
// for 'Test article 3 - dev' and 'Test article 4 - local_1'.
|
||||
$expected = [
|
||||
'node' => [
|
||||
5 => 3,
|
||||
7 => 4,
|
||||
],
|
||||
];
|
||||
$this->assertEquals($expected, $workspace_merger->getDifferringRevisionIdsOnSource());
|
||||
$this->assertEquals(2, $workspace_merger->getNumberOfChangesOnSource());
|
||||
|
||||
// Check that Local 1 can not be merged directly into Stage, since it can
|
||||
// only be merged into its direct parent.
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('The contents of a workspace can only be merged into its parent workspace.');
|
||||
$workspace_merger->merge();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\workspaces\Kernel;
|
||||
|
||||
use Drupal\workspaces\Entity\Workspace;
|
||||
|
||||
/**
|
||||
* A trait with common workspaces testing functionality.
|
||||
*/
|
||||
trait WorkspaceTestTrait {
|
||||
|
||||
/**
|
||||
* The workspaces manager.
|
||||
*
|
||||
* @var \Drupal\workspaces\WorkspaceManagerInterface
|
||||
*/
|
||||
protected $workspaceManager;
|
||||
|
||||
/**
|
||||
* An array of test workspaces, keyed by workspace ID.
|
||||
*
|
||||
* @var \Drupal\workspaces\WorkspaceInterface[]
|
||||
*/
|
||||
protected $workspaces = [];
|
||||
|
||||
/**
|
||||
* Enables the Workspaces module and creates two workspaces.
|
||||
*/
|
||||
protected function initializeWorkspacesModule() {
|
||||
// Enable the Workspaces module here instead of the static::$modules array
|
||||
// so we can test it with default content.
|
||||
$this->enableModules(['workspaces']);
|
||||
$this->container = \Drupal::getContainer();
|
||||
$this->entityTypeManager = \Drupal::entityTypeManager();
|
||||
$this->workspaceManager = \Drupal::service('workspaces.manager');
|
||||
|
||||
$this->installEntitySchema('workspace');
|
||||
$this->installSchema('workspaces', ['workspace_association']);
|
||||
|
||||
// Create two workspaces by default, 'live' and 'stage'.
|
||||
$this->workspaces['live'] = Workspace::create(['id' => 'live', 'label' => 'Live']);
|
||||
$this->workspaces['live']->save();
|
||||
$this->workspaces['stage'] = Workspace::create(['id' => 'stage', 'label' => 'Stage']);
|
||||
$this->workspaces['stage']->save();
|
||||
|
||||
$permissions = array_intersect([
|
||||
'administer nodes',
|
||||
'create workspace',
|
||||
'edit any workspace',
|
||||
'view any workspace',
|
||||
], array_keys($this->container->get('user.permissions')->getPermissions()));
|
||||
$this->setCurrentUser($this->createUser($permissions));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a given workspace as active.
|
||||
*
|
||||
* @param string $workspace_id
|
||||
* The ID of the workspace to switch to.
|
||||
*/
|
||||
protected function switchToWorkspace($workspace_id) {
|
||||
// Switch the test runner's context to the specified workspace.
|
||||
$workspace = $this->entityTypeManager->getStorage('workspace')->load($workspace_id);
|
||||
\Drupal::service('workspaces.manager')->setActiveWorkspace($workspace);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the following workspace hierarchy:
|
||||
* live
|
||||
* - stage
|
||||
* - dev
|
||||
* - local_1
|
||||
* - local_2
|
||||
* - qa
|
||||
*/
|
||||
protected function createWorkspaceHierarchy() {
|
||||
$this->workspaces['dev'] = Workspace::create(['id' => 'dev', 'parent' => 'stage']);
|
||||
$this->workspaces['dev']->save();
|
||||
$this->workspaces['local_1'] = Workspace::create(['id' => 'local_1', 'parent' => 'dev']);
|
||||
$this->workspaces['local_1']->save();
|
||||
$this->workspaces['local_2'] = Workspace::create(['id' => 'local_2', 'parent' => 'dev']);
|
||||
$this->workspaces['local_2']->save();
|
||||
$this->workspaces['qa'] = Workspace::create(['id' => 'qa', 'parent' => 'live']);
|
||||
$this->workspaces['qa']->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the workspace_association records for a test scenario.
|
||||
*
|
||||
* @param array $expected
|
||||
* An array of expected values, as defined in ::testWorkspaces().
|
||||
* @param string $entity_type_id
|
||||
* The ID of the entity type that is being tested.
|
||||
*/
|
||||
protected function assertWorkspaceAssociation(array $expected, $entity_type_id) {
|
||||
/** @var \Drupal\workspaces\WorkspaceAssociationInterface $workspace_association */
|
||||
$workspace_association = \Drupal::service('workspaces.association');
|
||||
foreach ($expected as $workspace_id => $expected_tracked_revision_ids) {
|
||||
$tracked_entities = $workspace_association->getTrackedEntities($workspace_id, $entity_type_id);
|
||||
$tracked_revision_ids = isset($tracked_entities[$entity_type_id]) ? $tracked_entities[$entity_type_id] : [];
|
||||
$this->assertEquals($expected_tracked_revision_ids, array_keys($tracked_revision_ids));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the revisions which are not associated with any workspace.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* An entity type ID to find revisions for.
|
||||
* @param int[]|string[]|null $entity_ids
|
||||
* (optional) An array of entity IDs to filter the results by. Defaults to
|
||||
* NULL.
|
||||
*
|
||||
* @return array
|
||||
* An array of entity IDs, keyed by revision IDs.
|
||||
*/
|
||||
protected function getUnassociatedRevisions($entity_type_id, $entity_ids = NULL) {
|
||||
$entity_type = \Drupal::entityTypeManager()->getDefinition($entity_type_id);
|
||||
|
||||
$query = \Drupal::entityTypeManager()
|
||||
->getStorage($entity_type_id)
|
||||
->getQuery()
|
||||
->allRevisions()
|
||||
->accessCheck(FALSE)
|
||||
->notExists($entity_type->get('revision_metadata_keys')['workspace']);
|
||||
|
||||
if ($entity_ids) {
|
||||
$query->condition($entity_type->getKey('id'), $entity_ids, 'IN');
|
||||
}
|
||||
|
||||
return $query->execute();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\workspaces\Unit;
|
||||
|
||||
use Drupal\path_alias\AliasManagerInterface;
|
||||
use Drupal\Core\Path\CurrentPathStack;
|
||||
use Drupal\Core\Routing\CacheableRouteProviderInterface;
|
||||
use Drupal\Core\Routing\RouteProviderInterface;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
use Drupal\workspaces\EventSubscriber\WorkspaceRequestSubscriber;
|
||||
use Drupal\workspaces\WorkspaceInterface;
|
||||
use Drupal\workspaces\WorkspaceManagerInterface;
|
||||
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\workspaces\EventSubscriber\WorkspaceRequestSubscriber
|
||||
*
|
||||
* @group workspace
|
||||
*/
|
||||
class WorkspaceRequestSubscriberTest extends UnitTestCase {
|
||||
|
||||
/**
|
||||
* @var \Drupal\path_alias\AliasManagerInterface
|
||||
*/
|
||||
protected $aliasManager;
|
||||
|
||||
/**
|
||||
* @var \Drupal\Core\Path\CurrentPathStack
|
||||
*/
|
||||
protected $currentPath;
|
||||
|
||||
/**
|
||||
* @var \Drupal\workspaces\WorkspaceManagerInterface
|
||||
*/
|
||||
protected $workspaceManager;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->aliasManager = $this->prophesize(AliasManagerInterface::class)->reveal();
|
||||
$this->currentPath = $this->prophesize(CurrentPathStack::class)->reveal();
|
||||
$this->workspaceManager = $this->prophesize(WorkspaceManagerInterface::class);
|
||||
|
||||
$active_workspace = $this->prophesize(WorkspaceInterface::class);
|
||||
$active_workspace->id()->willReturn('test');
|
||||
$this->workspaceManager->getActiveWorkspace()->willReturn($active_workspace->reveal());
|
||||
$this->workspaceManager->hasActiveWorkspace()->willReturn(TRUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::onKernelRequest
|
||||
*/
|
||||
public function testOnKernelRequestWithCacheableRouteProvider() {
|
||||
$route_provider = $this->prophesize(CacheableRouteProviderInterface::class);
|
||||
$route_provider->addExtraCacheKeyPart('workspace', 'test')->shouldBeCalled();
|
||||
|
||||
// Check that WorkspaceRequestSubscriber::onKernelRequest() calls
|
||||
// addExtraCacheKeyPart() on a route provider that implements
|
||||
// CacheableRouteProviderInterface.
|
||||
$workspace_request_subscriber = new WorkspaceRequestSubscriber($this->aliasManager, $this->currentPath, $route_provider->reveal(), $this->workspaceManager->reveal());
|
||||
$event = $this->prophesize(GetResponseEvent::class)->reveal();
|
||||
$this->assertNull($workspace_request_subscriber->onKernelRequest($event));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::onKernelRequest
|
||||
*/
|
||||
public function testOnKernelRequestWithoutCacheableRouteProvider() {
|
||||
$route_provider = $this->prophesize(RouteProviderInterface::class);
|
||||
|
||||
// Check that WorkspaceRequestSubscriber::onKernelRequest() doesn't call
|
||||
// addExtraCacheKeyPart() on a route provider that does not implement
|
||||
// CacheableRouteProviderInterface.
|
||||
$workspace_request_subscriber = new WorkspaceRequestSubscriber($this->aliasManager, $this->currentPath, $route_provider->reveal(), $this->workspaceManager->reveal());
|
||||
$event = $this->prophesize(GetResponseEvent::class)->reveal();
|
||||
$this->assertNull($workspace_request_subscriber->onKernelRequest($event));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
name: Workspaces
|
||||
type: module
|
||||
description: 'Allows users to stage content or preview a full site by using multiple workspaces on a single site.'
|
||||
version: VERSION
|
||||
core: 8.x
|
||||
package: Core (Experimental)
|
||||
configure: entity.workspace.collection
|
||||
dependencies:
|
||||
- drupal:user
|
||||
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains install, update and uninstall functions for the Workspaces module.
|
||||
*/
|
||||
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Entity\ContentEntityNullStorage;
|
||||
use Drupal\Core\Field\BaseFieldDefinition;
|
||||
use Drupal\workspaces\Entity\Workspace;
|
||||
|
||||
/**
|
||||
* Implements hook_requirements().
|
||||
*/
|
||||
function workspaces_requirements($phase) {
|
||||
$requirements = [];
|
||||
if ($phase === 'install') {
|
||||
if (\Drupal::moduleHandler()->moduleExists('workspace')) {
|
||||
$requirements['workspace_incompatibility'] = [
|
||||
'severity' => REQUIREMENT_ERROR,
|
||||
'description' => t('Workspaces can not be installed when the contributed Workspace module is also installed. See the <a href=":link">upgrade path</a> page for more information on how to upgrade.', [
|
||||
':link' => 'https://www.drupal.org/node/2987783',
|
||||
]),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $requirements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_module_preinstall().
|
||||
*/
|
||||
function workspaces_module_preinstall($module) {
|
||||
if ($module !== 'workspaces') {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var \Drupal\workspaces\WorkspaceManagerInterface $workspace_manager */
|
||||
$workspace_manager = \Drupal::service('workspaces.manager');
|
||||
$entity_definition_update_manager = \Drupal::entityDefinitionUpdateManager();
|
||||
foreach ($entity_definition_update_manager->getEntityTypes() as $entity_type) {
|
||||
$revision_metadata_keys = $entity_type->get('revision_metadata_keys');
|
||||
if ($workspace_manager->isEntityTypeSupported($entity_type)) {
|
||||
$entity_type->setRevisionMetadataKey('workspace', 'workspace');
|
||||
$entity_definition_update_manager->updateEntityType($entity_type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_install().
|
||||
*/
|
||||
function workspaces_install() {
|
||||
// Set the owner of these default workspaces to be first user which which has
|
||||
// the 'administrator' role. This way we avoid hard coding user ID 1 for sites
|
||||
// that prefer to not give it any special meaning.
|
||||
$admin_roles = \Drupal::entityTypeManager()->getStorage('user_role')->getQuery()
|
||||
->condition('is_admin', TRUE)
|
||||
->execute();
|
||||
if (!empty($admin_roles)) {
|
||||
$query = \Drupal::entityTypeManager()->getStorage('user')->getQuery()
|
||||
->condition('roles', $admin_roles, 'IN')
|
||||
->condition('status', 1)
|
||||
->sort('uid', 'ASC')
|
||||
->range(0, 1);
|
||||
$result = $query->execute();
|
||||
}
|
||||
|
||||
// Default to user ID 1 if we could not find any other administrator users.
|
||||
$owner_id = !empty($result) ? reset($result) : 1;
|
||||
|
||||
// Create a 'stage' workspace by default.
|
||||
Workspace::create([
|
||||
'id' => 'stage',
|
||||
'label' => 'Stage',
|
||||
'uid' => $owner_id,
|
||||
])->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_schema().
|
||||
*/
|
||||
function workspaces_schema() {
|
||||
$schema['workspace_association'] = [
|
||||
'description' => 'Stores the association between entity revisions and their workspace.',
|
||||
'fields' => [
|
||||
'workspace' => [
|
||||
'type' => 'varchar_ascii',
|
||||
'length' => 128,
|
||||
'not null' => TRUE,
|
||||
'default' => '',
|
||||
'description' => 'The workspace ID.',
|
||||
],
|
||||
'target_entity_type_id' => [
|
||||
'type' => 'varchar_ascii',
|
||||
'length' => EntityTypeInterface::ID_MAX_LENGTH,
|
||||
'not null' => TRUE,
|
||||
'default' => '',
|
||||
'description' => 'The ID of the associated entity type.',
|
||||
],
|
||||
'target_entity_id' => [
|
||||
'type' => 'int',
|
||||
'unsigned' => TRUE,
|
||||
'not null' => TRUE,
|
||||
'description' => 'The ID of the associated entity.',
|
||||
],
|
||||
'target_entity_revision_id' => [
|
||||
'type' => 'int',
|
||||
'unsigned' => TRUE,
|
||||
'not null' => TRUE,
|
||||
'description' => 'The revision ID of the associated entity.',
|
||||
],
|
||||
],
|
||||
'indexes' => [
|
||||
'target_entity_revision_id' => ['target_entity_revision_id'],
|
||||
],
|
||||
'primary key' => ['workspace', 'target_entity_type_id', 'target_entity_id'],
|
||||
];
|
||||
|
||||
return $schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the 'workspace' revision metadata field on all supported entity types.
|
||||
*/
|
||||
function workspaces_update_8801() {
|
||||
/** @var \Drupal\workspaces\WorkspaceManagerInterface $workspace_manager */
|
||||
$workspace_manager = \Drupal::service('workspaces.manager');
|
||||
$entity_definition_update_manager = \Drupal::entityDefinitionUpdateManager();
|
||||
foreach ($entity_definition_update_manager->getEntityTypes() as $entity_type_id => $entity_type) {
|
||||
if ($workspace_manager->isEntityTypeSupported($entity_type)) {
|
||||
$revision_metadata_keys = $entity_type->get('revision_metadata_keys');
|
||||
|
||||
if (!isset($revision_metadata_keys['workspace'])) {
|
||||
$revision_metadata_field_name = 'workspace';
|
||||
|
||||
// Bail out if there's an existing field called 'workspace'.
|
||||
if ($entity_definition_update_manager->getFieldStorageDefinition($revision_metadata_field_name, $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.");
|
||||
}
|
||||
|
||||
$entity_type->setRevisionMetadataKey('workspace', $revision_metadata_field_name);
|
||||
$entity_definition_update_manager->updateEntityType($entity_type);
|
||||
}
|
||||
else {
|
||||
$revision_metadata_field_name = $revision_metadata_keys['workspace'];
|
||||
}
|
||||
|
||||
$field_storage = BaseFieldDefinition::create('entity_reference')
|
||||
->setLabel(t('Workspace'))
|
||||
->setDescription(t('Indicates the workspace that this revision belongs to.'))
|
||||
->setSetting('target_type', 'workspace')
|
||||
->setInternal(TRUE)
|
||||
->setTranslatable(FALSE)
|
||||
->setRevisionable(TRUE);
|
||||
|
||||
$entity_definition_update_manager->installFieldStorageDefinition($revision_metadata_field_name, $entity_type_id, 'workspaces', $field_storage);
|
||||
}
|
||||
}
|
||||
|
||||
return t("The 'workspace' revision metadata field has been installed.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the 'parent' field to the 'workspace' entity type.
|
||||
*/
|
||||
function workspaces_update_8802() {
|
||||
$entity_definition_update_manager = \Drupal::entityDefinitionUpdateManager();
|
||||
|
||||
// Install the new 'parent' field.
|
||||
$storage_definition = BaseFieldDefinition::create('entity_reference')
|
||||
->setLabel(t('Parent'))
|
||||
->setDescription(t('The parent workspace.'))
|
||||
->setSetting('target_type', 'workspace')
|
||||
->setReadOnly(TRUE);
|
||||
|
||||
$entity_definition_update_manager->installFieldStorageDefinition('parent', 'workspace', 'workspaces', $storage_definition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the Workspace Association entity storage if necessary.
|
||||
*/
|
||||
function workspaces_update_8803() {
|
||||
$entity_definition_update_manager = \Drupal::entityDefinitionUpdateManager();
|
||||
$entity_type = $entity_definition_update_manager->getEntityType('workspace_association');
|
||||
|
||||
// We can't migrate the workspace association data if the entity type is not
|
||||
// using its default storage.
|
||||
// @see workspaces_post_update_move_association_data()
|
||||
if ($entity_type && $entity_type->getHandlerClasses()['storage'] === 'Drupal\workspaces\WorkspaceAssociationStorage') {
|
||||
\Drupal::state()->set('workspaces_update_8803.tables', [
|
||||
'base_table' => $entity_type->getBaseTable(),
|
||||
'revision_table' => $entity_type->getRevisionTable(),
|
||||
]);
|
||||
$entity_type->setStorageClass(ContentEntityNullStorage::class);
|
||||
$entity_definition_update_manager->uninstallEntityType($entity_type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
drupal.workspaces.toolbar:
|
||||
version: VERSION
|
||||
css:
|
||||
theme:
|
||||
css/workspaces.toolbar.css: {}
|
||||
drupal.workspaces.overview:
|
||||
version: VERSION
|
||||
css:
|
||||
theme:
|
||||
css/workspaces.overview.css: {}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user