Skip to content

Commit

Permalink
Merge pull request #3665 from pKallert/feature/workspaceSync
Browse files Browse the repository at this point in the history
FEATURE: Add workspace Sync button
  • Loading branch information
mhsdesign authored Dec 22, 2023
2 parents 316f498 + f9059da commit 654072c
Show file tree
Hide file tree
Showing 23 changed files with 599 additions and 9 deletions.
35 changes: 35 additions & 0 deletions Classes/Controller/BackendServiceController.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use Neos\ContentRepository\Core\Feature\WorkspacePublication\Command\PublishIndividualNodesFromWorkspace;
use Neos\ContentRepository\Core\Feature\WorkspacePublication\Dto\NodeIdsToPublishOrDiscard;
use Neos\ContentRepository\Core\Feature\WorkspacePublication\Dto\NodeIdToPublishOrDiscard;
use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Command\RebaseWorkspace;
use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints;
use Neos\ContentRepository\Core\SharedModel\Exception\NodeAggregateCurrentlyDoesNotExist;
use Neos\ContentRepository\Core\SharedModel\Workspace\WorkspaceName;
Expand Down Expand Up @@ -56,6 +57,8 @@

class BackendServiceController extends ActionController
{
use TranslationTrait;

/**
* @var array<int,string>
*/
Expand Down Expand Up @@ -618,4 +621,36 @@ public function generateUriPathSegmentAction(string $contextNode, string $text):
$slug = $this->nodeUriPathSegmentGenerator->generateUriPathSegment($contextNode, $text);
$this->view->assign('value', $slug);
}

/**
* Rebase user workspace to current workspace
*
* @param string $targetWorkspaceName
* @return void
*/
public function rebaseWorkspaceAction(string $targetWorkspaceName): void
{
$contentRepositoryId = SiteDetectionResult::fromRequest($this->request->getHttpRequest())->contentRepositoryId;
$contentRepository = $this->contentRepositoryRegistry->get($contentRepositoryId);

$command = RebaseWorkspace::create(WorkspaceName::fromString($targetWorkspaceName));
try {
$contentRepository->handle($command)->block();
} catch (\Exception $exception) {
$error = new Error();
$error->setMessage($error->getMessage());

$this->feedbackCollection->add($error);
$this->view->assign('value', $this->feedbackCollection);
return;
}

$success = new Success();
$success->setMessage(
$this->getLabel('workspaceSynchronizationApplied', ['workspaceName' => $targetWorkspaceName])
);
$this->feedbackCollection->add($success);

$this->view->assign('value', $this->feedbackCollection);
}
}
42 changes: 42 additions & 0 deletions Classes/Controller/TranslationTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

/*
* This file is part of the Neos.Neos.Ui package.
*
* (c) Contributors of the Neos Project - www.neos.io
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/

declare(strict_types=1);

namespace Neos\Neos\Ui\Controller;

use Neos\Flow\Annotations as Flow;
use Neos\Flow\I18n\Translator;

/**
* A trait to do easy backend module translations
*/
trait TranslationTrait
{
#[Flow\Inject]
protected Translator $translator;

/**
* @param array<int|string,mixed> $arguments
*/
public function getLabel(string $id, array $arguments = []): string
{
return $this->translator->translateById(
$id,
$arguments,
null,
null,
'Main',
'Neos.Neos.Ui'
) ?: $id;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,8 @@ public function serializePayload(ControllerContext $controllerContext)
$this->workspaceName,
$this->contentRepositoryId
),
'baseWorkspace' => $workspace->baseWorkspaceName->value
'baseWorkspace' => $workspace->baseWorkspaceName->value,
'status' => $workspace->status
] : [];
}
}
3 changes: 2 additions & 1 deletion Classes/Fusion/Helper/WorkspaceHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ public function getPersonalWorkspace(ContentRepositoryId $contentRepositoryId):
'baseWorkspace' => $personalWorkspace->baseWorkspaceName,
// TODO: FIX readonly flag!
//'readOnly' => !$this->domainUserService->currentUserCanPublishToWorkspace($baseWorkspace)
'readOnly' => false
'readOnly' => false,
'status' => $personalWorkspace->status->value
]
: [];
}
Expand Down
7 changes: 7 additions & 0 deletions Configuration/Routes.Service.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@
'@action': 'changeBaseWorkspace'
httpMethods: ['POST']

-
name: 'Rebase Workspace'
uriPattern: 'rebase-workspace'
defaults:
'@controller': 'BackendService'
'@action': 'rebaseWorkspace'
httpMethods: ['POST']
-
name: 'Copy nodes to clipboard'
uriPattern: 'copy-nodes'
Expand Down
3 changes: 3 additions & 0 deletions Resources/Private/Fusion/Backend/Root.fusion
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ backend = Neos.Fusion:Template {
changeBaseWorkspace = Neos.Fusion:UriBuilder {
action = 'changeBaseWorkspace'
}
rebaseWorkspace = Neos.Fusion:UriBuilder {
action = 'rebaseWorkspace'
}
copyNodes = Neos.Fusion:UriBuilder {
action = 'copyNodes'
}
Expand Down
20 changes: 20 additions & 0 deletions Resources/Private/Translations/en/Main.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,26 @@
<trans-unit id="deleteXNodes" xml:space="preserve">
<source>Delete {amount} nodes</source>
</trans-unit>
<trans-unit id="workspaceSynchronizationApplied" xml:space="preserve">
<source>Successfully synced "{workspaceName}" workspace.</source>
</trans-unit>
<trans-unit id="syncPersonalWorkSpace" xml:space="preserve">
<source>Synchronize personal workspace</source>
</trans-unit>
<trans-unit id="syncPersonalWorkSpaceConfirm" xml:space="preserve">
<source>Synchronize now</source>
</trans-unit>
<trans-unit id="syncPersonalWorkSpaceMessage" xml:space="preserve">
<source>Your personal workspace is up-to-date with the current workspace.</source>
</trans-unit>
<trans-unit id="syncPersonalWorkSpaceMessageOutdated" xml:space="preserve">
<source>It seems like there are changes in the workspace that are not reflected in your personal workspace.
You should synchronize your personal workspace to avoid conflicts.</source>
</trans-unit>
<trans-unit id="syncPersonalWorkSpaceMessageOutdatedConflict" xml:space="preserve">
<source>It seems like there are changes in the workspace that are not reflected in your personal workspace.
The changes lead to an error state. Please contact your administrator to resolve the problem.</source>
</trans-unit>
<trans-unit id="rangeEditorMinimum" xml:space="preserve">
<source>Minimum</source>
</trans-unit>
Expand Down
6 changes: 6 additions & 0 deletions packages/neos-ts-interfaces/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,12 @@ export enum SelectionModeTypes {
RANGE_SELECT = 'RANGE_SELECT'
}

export enum WorkspaceStatus {
UP_TO_DATE = 'UP_TO_DATE',
OUTDATED = 'OUTDATED',
OUTDATED_CONFLICT = 'OUTDATED_CONFLICT'
}

export interface ValidatorConfiguration {
[propName: string]: any;
}
Expand Down
17 changes: 17 additions & 0 deletions packages/neos-ui-backend-connector/src/Endpoints/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface Routes {
publish: string;
discard: string;
changeBaseWorkspace: string;
rebaseWorkspace: string;
copyNodes: string;
cutNodes: string;
clearClipboard: string;
Expand Down Expand Up @@ -111,6 +112,21 @@ export default (routes: Routes) => {
})).then(response => fetchWithErrorHandling.parseJson(response))
.catch(reason => fetchWithErrorHandling.generalErrorHandler(reason));

const rebaseWorkspace = (targetWorkspaceName: WorkspaceName) => fetchWithErrorHandling.withCsrfToken(csrfToken => ({
url: routes.ui.service.rebaseWorkspace,

method: 'POST',
credentials: 'include',
headers: {
'X-Flow-Csrftoken': csrfToken,
'Content-Type': 'application/json'
},
body: JSON.stringify({
targetWorkspaceName
})
})).then(response => fetchWithErrorHandling.parseJson(response))
.catch(reason => fetchWithErrorHandling.generalErrorHandler(reason));

const copyNodes = (nodes: NodeContextPath[]) => fetchWithErrorHandling.withCsrfToken(csrfToken => ({
url: routes.ui.service.copyNodes,

Expand Down Expand Up @@ -660,6 +676,7 @@ export default (routes: Routes) => {
publish,
discard,
changeBaseWorkspace,
rebaseWorkspace,
copyNodes,
cutNodes,
clearClipboard,
Expand Down
2 changes: 2 additions & 0 deletions packages/neos-ui-redux-store/src/CR/Workspaces/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ test(`should export actionTypes`, () => {
expect(typeof (actionTypes.DISCARD_ABORTED)).toBe('string');
expect(typeof (actionTypes.DISCARD_CONFIRMED)).toBe('string');
expect(typeof (actionTypes.CHANGE_BASE_WORKSPACE)).toBe('string');
expect(typeof (actionTypes.REBASE_WORKSPACE)).toBe('string');
});

test(`should export action creators`, () => {
Expand All @@ -19,6 +20,7 @@ test(`should export action creators`, () => {
expect(typeof (actions.abortDiscard)).toBe('function');
expect(typeof (actions.confirmDiscard)).toBe('function');
expect(typeof (actions.changeBaseWorkspace)).toBe('function');
expect(typeof (actions.rebaseWorkspace)).toBe('function');
});

test(`should export a reducer`, () => {
Expand Down
15 changes: 12 additions & 3 deletions packages/neos-ui-redux-store/src/CR/Workspaces/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export interface WorkspaceInformation {
}>;
baseWorkspace: WorkspaceName;
readOnly?: boolean;
status?: string;
}

export interface State extends Readonly<{
Expand All @@ -27,7 +28,8 @@ export const defaultState: State = {
personalWorkspace: {
name: '',
publishableNodes: [],
baseWorkspace: ''
baseWorkspace: '',
status: ''
},
toBeDiscarded: []
};
Expand All @@ -38,7 +40,8 @@ export enum actionTypes {
COMMENCE_DISCARD = '@neos/neos-ui/CR/Workspaces/COMMENCE_DISCARD',
DISCARD_ABORTED = '@neos/neos-ui/CR/Workspaces/DISCARD_ABORTED',
DISCARD_CONFIRMED = '@neos/neos-ui/CR/Workspaces/DISCARD_CONFIRMED',
CHANGE_BASE_WORKSPACE = '@neos/neos-ui/CR/Workspaces/CHANGE_BASE_WORKSPACE'
CHANGE_BASE_WORKSPACE = '@neos/neos-ui/CR/Workspaces/CHANGE_BASE_WORKSPACE',
REBASE_WORKSPACE = '@neos/neos-ui/CR/Workspaces/REBASE_WORKSPACE'
}

export type Action = ActionType<typeof actions>;
Expand Down Expand Up @@ -75,6 +78,11 @@ const confirmDiscard = () => createAction(actionTypes.DISCARD_CONFIRMED);
*/
const changeBaseWorkspace = (name: string) => createAction(actionTypes.CHANGE_BASE_WORKSPACE, name);

/**
* Rebase the user workspace
*/
const rebaseWorkspace = (name: string) => createAction(actionTypes.REBASE_WORKSPACE, name);

//
// Export the actions
//
Expand All @@ -84,7 +92,8 @@ export const actions = {
commenceDiscard,
abortDiscard,
confirmDiscard,
changeBaseWorkspace
changeBaseWorkspace,
rebaseWorkspace
};

//
Expand Down
11 changes: 9 additions & 2 deletions packages/neos-ui-redux-store/src/CR/Workspaces/selectors.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import {createSelector} from 'reselect';
import {documentNodeContextPathSelector} from '../Nodes/selectors';
import {GlobalState} from '../../System';
import {NodeContextPath} from '@neos-project/neos-ts-interfaces';
import {NodeContextPath, WorkspaceStatus} from '@neos-project/neos-ts-interfaces';

export const personalWorkspaceNameSelector = (state: GlobalState) => state?.cr?.workspaces?.personalWorkspace?.name;

export const personalWorkspaceRebaseStatusSelector = (state: GlobalState) => state?.cr?.workspaces?.personalWorkspace?.status;

export const baseWorkspaceSelector = (state: GlobalState) => state?.cr?.workspaces?.personalWorkspace?.baseWorkspace;

export const isWorkspaceReadOnlySelector = (state: GlobalState) => state?.cr?.workspaces?.personalWorkspace?.readOnly || false;
export const isWorkspaceReadOnlySelector = (state: GlobalState) => {
if (state?.cr?.workspaces?.personalWorkspace?.status === WorkspaceStatus.OUTDATED_CONFLICT) {
return true;
}
return state?.cr?.workspaces?.personalWorkspace?.readOnly || false
};

export const publishableNodesSelector = (state: GlobalState) => state?.cr?.workspaces?.personalWorkspace?.publishableNodes;

Expand Down
28 changes: 26 additions & 2 deletions packages/neos-ui-redux-store/src/UI/Remote/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ import {NodeContextPath} from '@neos-project/neos-ts-interfaces';
export interface State extends Readonly<{
isSaving: boolean,
isPublishing: boolean,
isDiscarding: boolean
isDiscarding: boolean,
isSyncing: boolean
}> {}

export const defaultState: State = {
isSaving: false,
isPublishing: false,
isDiscarding: false
isDiscarding: false,
isSyncing: false
};

//
Expand All @@ -28,6 +30,8 @@ export enum actionTypes {
FINISH_PUBLISHING = '@neos/neos-ui/UI/Remote/FINISH_PUBLISHING',
START_DISCARDING = '@neos/neos-ui/UI/Remote/START_DISCARDING',
FINISH_DISCARDING = '@neos/neos-ui/UI/Remote/FINISH_DISCARDING',
START_SYNCHRONIZATION = '@neos/neos-ui/UI/Remote/START_SYNCHRONIZATION',
FINISH_SYNCHRONIZATION = '@neos/neos-ui/UI/Remote/FINISH_SYNCHRONIZATION',
DOCUMENT_NODE_CREATED = '@neos/neos-ui/UI/Remote/DOCUMENT_NODE_CREATED'
}

Expand Down Expand Up @@ -61,6 +65,16 @@ const startDiscarding = () => createAction(actionTypes.START_DISCARDING);
*/
const finishDiscarding = () => createAction(actionTypes.FINISH_DISCARDING);

/**
* Marks an ongoing synchronization process.
*/
const startSynchronization = () => createAction(actionTypes.START_SYNCHRONIZATION);

/**
* Marks that an ongoing synchronization process has finished.
*/
const finishSynchronization = () => createAction(actionTypes.FINISH_SYNCHRONIZATION);

/**
* Marks that an publishing process has been locked.
*/
Expand Down Expand Up @@ -88,6 +102,8 @@ export const actions = {
finishPublishing,
startDiscarding,
finishDiscarding,
startSynchronization,
finishSynchronization,
documentNodeCreated
};

Expand Down Expand Up @@ -130,6 +146,14 @@ export const reducer = (state: State = defaultState, action: InitAction | Action
draft.isDiscarding = false;
break;
}
case actionTypes.START_SYNCHRONIZATION: {
draft.isSyncing = true;
break;
}
case actionTypes.FINISH_SYNCHRONIZATION: {
draft.isSyncing = false;
break;
}
}
});

Expand Down
Loading

0 comments on commit 654072c

Please sign in to comment.