Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEATURE: Add workspace Sync button #3665

Merged
merged 11 commits into from
Dec 22, 2023
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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there no reducer needed for REBASE_WORKSPACE? i guess because its handled via saga?


//
// 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
Loading