Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision

Target

Select target project
  • project/workspaces_parallel
  • issue/workspaces_parallel-3473358
  • issue/workspaces_parallel-3483880
  • issue/workspaces_parallel-3483881
  • issue/workspaces_parallel-3491027
5 results
Select Git revision
Show changes
Commits on Source (8)
Showing
with 614 additions and 2 deletions
################
# DrupalCI GitLabCI template
#
# Gitlab-ci.yml to replicate DrupalCI testing for Contrib
#
# With thanks to:
# * The GitLab Acceleration Initiative participants
# * DrupalSpoons
################
################
# Guidelines
#
# This template is designed to give any Contrib maintainer everything they need to test, without requiring modification. It is also designed to keep up to date with Core Development automatically through the use of include files that can be centrally maintained.
#
# However, you can modify this template if you have additional needs for your project.
################
################
# Includes
#
# Additional configuration can be provided through includes.
# One advantage of include files is that if they are updated upstream, the changes affect all pipelines using that include.
#
# Includes can be overridden by re-declaring anything provided in an include, here in gitlab-ci.yml
# https://docs.gitlab.com/ee/ci/yaml/includes.html#override-included-configuration-values
################
include:
################
# DrupalCI includes:
# As long as you include this, any future includes added by the Drupal Association will be accessible to your pipelines automatically.
# View these include files at https://git.drupalcode.org/project/gitlab_templates/
################
- project: $_GITLAB_TEMPLATES_REPO
ref: $_GITLAB_TEMPLATES_REF
file:
- '/includes/include.drupalci.main.yml'
# EXPERIMENTAL: For Drupal 7, remove the above line and uncomment the below.
# - '/includes/include.drupalci.main-d7.yml'
- '/includes/include.drupalci.variables.yml'
- '/includes/include.drupalci.workflows.yml'
################
# Pipeline configuration variables
#
# These are the variables provided to the Run Pipeline form that a user may want to override.
#
# Docs at https://git.drupalcode.org/project/gitlab_templates/-/blob/1.0.x/includes/include.drupalci.variables.yml
################
# variables:
# SKIP_ESLINT: '1'
###################################################################################
#
# *
# /(
# ((((,
# /(((((((
# ((((((((((*
# ,(((((((((((((((
# ,(((((((((((((((((((
# ((((((((((((((((((((((((*
# *(((((((((((((((((((((((((((((
# ((((((((((((((((((((((((((((((((((*
# *(((((((((((((((((( .((((((((((((((((((
# ((((((((((((((((((. /(((((((((((((((((*
# /((((((((((((((((( .(((((((((((((((((,
# ,(((((((((((((((((( ((((((((((((((((((
# .(((((((((((((((((((( .(((((((((((((((((
# ((((((((((((((((((((((( ((((((((((((((((/
# (((((((((((((((((((((((((((/ ,(((((((((((((((*
# .((((((((((((((/ /(((((((((((((. ,(((((((((((((((
# *(((((((((((((( ,(((((((((((((/ *((((((((((((((.
# ((((((((((((((, /(((((((((((((. ((((((((((((((,
# (((((((((((((/ ,(((((((((((((* ,(((((((((((((,
# *((((((((((((( .((((((((((((((( ,(((((((((((((
# ((((((((((((/ /((((((((((((((((((. ,((((((((((((/
# ((((((((((((( *(((((((((((((((((((((((* *((((((((((((
# ((((((((((((( ,(((((((((((((..((((((((((((( *((((((((((((
# ((((((((((((, /((((((((((((* /((((((((((((/ ((((((((((((
# ((((((((((((( /((((((((((((/ (((((((((((((* ((((((((((((
# (((((((((((((/ /(((((((((((( ,((((((((((((, *((((((((((((
# (((((((((((((( *(((((((((((/ *((((((((((((. ((((((((((((/
# *((((((((((((((((((((((((((, /(((((((((((((((((((((((((
# ((((((((((((((((((((((((( ((((((((((((((((((((((((,
# .(((((((((((((((((((((((/ ,(((((((((((((((((((((((
# ((((((((((((((((((((((/ ,(((((((((((((((((((((/
# *((((((((((((((((((((( (((((((((((((((((((((,
# ,(((((((((((((((((((((, ((((((((((((((((((((/
# ,(((((((((((((((((((((* /((((((((((((((((((((
# ((((((((((((((((((((((, ,/((((((((((((((((((((,
# ,(((((((((((((((((((((((((((((((((((((((((((((((((((
# .(((((((((((((((((((((((((((((((((((((((((((((
# .((((((((((((((((((((((((((((((((((((,.
# .,(((((((((((((((((((((((((.
#
###################################################################################
...@@ -7,6 +7,6 @@ use Symfony\Component\Validator\Constraint; ...@@ -7,6 +7,6 @@ use Symfony\Component\Validator\Constraint;
class ParallelWorkspaceConflictConstraintValidator extends EntityWorkspaceConflictConstraintValidator { class ParallelWorkspaceConflictConstraintValidator extends EntityWorkspaceConflictConstraintValidator {
public function validate($entity, Constraint $constraint) {} public function validate($entity, Constraint $constraint): void {}
} }
<?php
declare(strict_types=1);
namespace Drupal\Tests\workspaces_parallel\Functional;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\workspaces\Functional\WorkspaceTestUtilities;
/**
* Tests concurrent edits in different workspaces.
*
* @group workspaces
*/
class WorkspacesParallelConcurrentEditingTest extends BrowserTestBase {
use WorkspaceTestUtilities;
/**
* {@inheritdoc}
*/
protected static $modules = ['block', 'node', 'workspaces', 'workspaces_parallel'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests editing a node in multiple workspaces.
*/
public function testConcurrentEditing(): void {
// 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 still edit
// the node.
$this->switchToWorkspace($gravity);
$this->drupalGet('/node/' . $test_node->id() . '/edit');
$page = $this->getSession()->getPage();
$this->assertTrue($page->hasField('title[0][value]'));
// Check that the node passes validation for API calls.
$violations = $test_node->validate();
$this->assertCount(0, $violations);
// Switch to the Live version of the site and check that the user still can
// edit the node.
$this->switchToLive();
$this->drupalGet('/node/' . $test_node->id() . '/edit');
$page = $this->getSession()->getPage();
$this->assertTrue($page->hasField('title[0][value]'));
// Check that the node passes validation for API calls.
$violations = $test_node->validate();
$this->assertCount(0, $violations);
}
}
<?php
declare(strict_types=1);
namespace Drupal\Tests\workspaces_parallel\Kernel;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\node\Traits\NodeCreationTrait;
use Drupal\Tests\user\Traits\UserCreationTrait;
use Drupal\Tests\workspaces\Kernel\WorkspaceTestTrait;
/**
* Test description.
*
* @group workspaces_parallel
*/
final class WorkspacesParallelTest extends KernelTestBase {
private $entityTypeManager;
use UserCreationTrait;
use NodeCreationTrait;
use WorkspaceTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'system',
'node',
'user',
'workspaces',
'workspaces_parallel',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->entityTypeManager = \Drupal::entityTypeManager();
$this->installEntitySchema('node');
$this->installEntitySchema('user');
$this->installEntitySchema('workspace');
}
/**
* Test that the live workspace only pulls in a non-workspace revision during
* editing when loaded with EntityRepository::getActive().
*/
public function testParallelEditing(): void {
$this->initializeWorkspacesModule();
$node = \Drupal::entityTypeManager()->getStorage('node')->create([
'title' => 'Change in the live site',
'type' => 'page',
]);
$node->save();
$stage_ws = \Drupal::entityTypeManager()
->getStorage('workspace')
->load('stage');
$ws_manager = \Drupal::service('workspaces.manager');
$ws_manager->setActiveWorkspace($stage_ws);
$node->title = 'Change in stage site';
$node->save();
$ws_manager->switchToLive();
$nR = \Drupal::service('entity.repository')->getActive('node', $node->id());
// We should see the live site change, not the stage one, even though that
// is more recent.
self::assertEquals('Change in the live site', $nR->label());
}
}
name: 'Parallel Workspaces' name: 'Parallel Workspaces'
type: module type: module
description: 'Support for parallel workspaces (content in multiple workspaces)' description: 'Support for parallel workspaces (content in multiple workspaces)'
core_version_requirement: ^10 core_version_requirement: ^10 || ^11
dependencies: dependencies:
- workspaces - workspaces
/**
* Styles for the Revision Graph Module
*
* This file contains all styles for the revision graph functionality,
* including the main container, progress indicators, and error states.
* Follows BEM naming conventions with "revision-graph" as the parent class.
*/
/* -----------------------------------------------------------------------------
* Main Container Styles
* -------------------------------------------------------------------------- */
.revision-graph-wrapper {
display: flex;
flex-direction: column;
width: 100%;
max-width: 1200px;
margin: auto;
height: 100%;
min-height: 600px;
background: var(--bg-primary, #ffffff);
border: 1px solid var(--border-color, #e1e4e8);
overflow: hidden;
}
/* Main content grid layout */
.revision-graph-wrapper__content {
display: grid;
grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
height: 100%;
}
/* Individual section containers */
.revision-graph-wrapper__container {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
border-radius: 0;
border: none;
padding: 0;
}
/* Section header styling */
.revision-graph-wrapper__container__header {
padding: 16px;
border-bottom: 1px solid var(--border-color, #e1e4e8);
background: var(--header-bg, #f6f8fa);
}
/* Section header title */
.revision-graph-wrapper__container__header__title {
margin: 0;
font-size: 14px;
font-weight: 600;
color: var(--text-primary, #24292e);
}
/* -----------------------------------------------------------------------------
* Progress Indicator Styles
* -------------------------------------------------------------------------- */
/* Progress container */
.revision-graph__progress {
width: 100%;
height: 24px;
background-color: #f0f0f0;
border-radius: 4px;
margin: 10px 0;
overflow: hidden;
position: relative;
}
/* Progress bar element */
.revision-graph__progress-bar {
height: 100%;
width: 0%;
background-color: #0074bd;
transition: width 0.3s ease;
}
/* Progress state: low */
.revision-graph__progress-bar--low {
background-color: #e09600; /* Amber/warning color */
}
/* Progress state: medium */
.revision-graph__progress-bar--medium {
background-color: #0074bd; /* Blue/information color */
}
/* Progress state: high */
.revision-graph__progress-bar--high {
background-color: #77b259; /* Green/success color */
}
/* Progress text overlay */
.revision-graph__progress-text {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 24px;
line-height: 24px;
text-align: center;
font-size: 12px;
font-weight: bold;
white-space: nowrap;
text-shadow: 0px 0px 2px rgba(0, 0, 0, 0.7);
color: #fff;
}
/* -----------------------------------------------------------------------------
* Error Message Styles
* -------------------------------------------------------------------------- */
/* Error message container */
.revision-graph__error {
padding: 10px;
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
border-radius: 4px;
margin: 10px 0;
}
/**
* @file
* Parallel Workspaces behaviors.
*/
(function (Drupal) {
'use strict';
Drupal.behaviors.workspacesParallelGraph = {
attach(context, settings) {
const createGraphConfig = function () {
return {
width: 720,
nodeHeight: 40,
nodeWidth: 20,
columnWidth: 100,
colors: [
'#a43631', // Red
'#d07e3f', // Orange
'#6209d4', // Purple
'#08c6cd', // Teal
'#e136a7', // Pink
'#076cfd', // Blue
'#8e4299', // Violet
],
showLabels: true,
theme: 'light',
branchColors: new Map(),
// UI text customization with translations
uiText: {
revisionTerm: Drupal.t('Revision'),
historyTerm: Drupal.t('Revision history'),
graphVisualizationLabel: Drupal.t('Revision graph visualization'),
interactiveGraphLabel: Drupal.t('Interactive revision graph'),
graphDescription: Drupal.t('Visual representation of content revision history'),
detailsLabel: Drupal.t('Revision details'),
closeButtonLabel: Drupal.t('Close revision details'),
actionsLabel: Drupal.t('Revision actions'),
historyListLabel: Drupal.t('Revision history'),
},
};
};
once('workspaces-parallel-graph', 'body', context).forEach(() => {
fetch('workspaces-graph-items')
.then(response => response.json())
.then(data => {
const graphCommits = [];
const listCommits = [];
data.forEach(commit => {
const trimmedBranch = commit.branch && commit.branch.length > 15
? commit.branch.slice(0, 12) + '...'
: commit.branch;
graphCommits.push({
id: commit.id,
message: commit.message,
author: commit.author,
timestamp: commit.timestamp,
parents: commit.parents || [],
refs: trimmedBranch ? [trimmedBranch] : [],
tags: commit.tags || [],
urls: commit.urls || [],
});
listCommits.push({
id: commit.id,
message: commit.message,
author: commit.author,
timestamp: commit.timestamp,
parents: commit.parents || [],
refs: commit.branch ? [commit.branch] : [],
tags: commit.tags || [],
urls: commit.urls || [],
});
});
// Get graph containers
const graphNode = document.getElementById('js-revision-graph--graph');
const listNode = document.getElementById('js-revision-graph--list');
// Initialize the graph with configuration
const graph = new RevisionGraph(createGraphConfig());
// Render graph and list views
graph.renderGraph(graphNode, graphCommits);
graph.renderList(listNode, listCommits);
});
});
},
};
}(Drupal));
<?php
declare(strict_types=1);
namespace Drupal\workspaces_parallel_graph\Controller;
use Drupal\node\NodeInterface;
use Drupal\revision_graph\Controller\RevisionGraphController;
use Symfony\Component\HttpFoundation\JsonResponse;
/**r
* Returns responses for Parallel Workspaces routes.
*/
class WorkspacesParallelGraphController extends RevisionGraphController {
/**
* {@inheritDoc}
*/
public function revisions(NodeInterface $node): JsonResponse {
$node_storage = \Drupal::entityTypeManager()->getStorage('node');
$revision_ids = array_reverse($this->getRevisionIds($node, $node_storage, 0, 100));
$out = [];
// This is in reverse order, so the oldest revision is first. Easier to
// track parents.
$prevCommits = [];
foreach ($revision_ids as $vid) {
/** @var \Drupal\node\NodeInterface $revision */
$revision = $node_storage->loadRevision($vid);
// The "branch" is the workspace label.
$branch = $revision->get('workspace')->entity->label->value ?? 'live';
if (!$revision->get('workspace')->isEmpty()) {
if ($revision->get('workspace')->entity->status->value == 'closed') {
// Do not include revisions associated with closed workspaces.
continue;
}
}
if (!isset($prevCommits[$branch])) {
// No previous commits on this branch, so we need to use the latest live
// one as the parent.
$parent = $prevCommits['live']['id'] ?? NULL;
}
else {
// We have a previous commit on this branch, so we can use it as the
// parent.
$parent = $prevCommits[$branch]['id'] ?? NULL;
}
$commit = [
'id' => $revision->getRevisionId(),
'author' => $revision->getRevisionUser()->getDisplayName(),
'timestamp' => $revision->getRevisionCreationTime(),
'message' => $revision->getRevisionLogMessage() ?? '',
'branch' => $branch,
'parents' => $parent ? [$parent] : [],
'tags' => [],
];
// Store last commit on the branch.
$prevCommits[$branch] = $commit;
$out[$revision->getRevisionId()] = $commit;
}
return new JsonResponse(array_reverse($out));
}
/**
* {@inheritDoc}
*/
public function revisionOverview(NodeInterface $node) {
return [
'#theme' => 'revision_graph',
'#total' => $this->getTotalRevisions($node),
'#limit' => self::PAGE_LIMIT,
'#attached' => [
'library' => 'workspaces_parallel_graph/graph',
],
];
}
}
name: Parallel Workspaces graph
type: module
description: 'Add a pretty graph on nodes to make parallel workspaces less confusing'
core_version_requirement: ^10 || ^11
dependencies:
- workspaces
- revision_graph
graph:
js:
js/graph.js: {}
css:
component:
css/graph.css: {}
dependencies:
- core/drupalSettings
- revision_graph/renderer
workspaces_parallel.graph:
title: Workspaces graph
route_name: workspaces_parallel.graph
base_route: entity.node.canonical
workspaces_parallel.graph:
path: '/node/{node}/workspaces-graph'
defaults:
_title: 'Workspaces graph'
_controller: '\Drupal\workspaces_parallel_graph\Controller\WorkspacesParallelGraphController::revisionOverview'
requirements:
_entity_access: 'node.view all revisions'
node: \d+
methods: [GET]
options:
_node_operation_route: TRUE
workspaces_parallel.graph_items:
path: '/node/{node}/workspaces-graph-items'
defaults:
_title: 'Workspaces graph items'
_controller: '\Drupal\workspaces_parallel_graph\Controller\WorkspacesParallelGraphController::revisions'
requirements:
_entity_access: 'node.view all revisions'
node: \d+
methods: [GET]
options:
_node_operation_route: TRUE