Skip to content

Commit

Permalink
TASK: Implement DISCARD_ALL strategy for conflict resolution during r…
Browse files Browse the repository at this point in the history
…ebase
  • Loading branch information
grebaldi committed Apr 8, 2024
1 parent 569d4bd commit 5c51d02
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 60 deletions.
4 changes: 3 additions & 1 deletion packages/neos-ui-redux-store/src/CR/Syncing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export type State = null | {
| { phase: SyncingPhase.ONGOING }
| {
phase: SyncingPhase.CONFLICT;
strategy: null | ResolutionStrategy;
conflicts: Conflict[];
}
| {
Expand Down Expand Up @@ -198,7 +199,8 @@ export const reducer = (state: State = defaultState, action: Action): State => {
return {
process: {
phase: SyncingPhase.CONFLICT,
conflicts: action.payload.conflicts
conflicts: action.payload.conflicts,
strategy: null
}
};
case actionTypes.RESOLUTION_STARTED:
Expand Down
151 changes: 105 additions & 46 deletions packages/neos-ui-sagas/src/Sync/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,27 @@ import {DimensionCombination, WorkspaceName} from '@neos-project/neos-ts-interfa
import {AnyError} from '@neos-project/neos-ui-error';
import {actionTypes, actions, selectors} from '@neos-project/neos-ui-redux-store';
import backend from '@neos-project/neos-ui-backend-connector';
import {PublishingMode, PublishingScope} from '@neos-project/neos-ui-redux-store/src/CR/Publishing';
import {Conflict, ResolutionStrategy} from '@neos-project/neos-ui-redux-store/src/CR/Syncing';
import {WorkspaceInformation} from '@neos-project/neos-ui-redux-store/src/CR/Workspaces';

// @TODO: This is a helper to gain type access to the available backend endpoints.
// It shouldn't be necessary to do this, and this hack should be removed once a
// better type API is available
import {default as Endpoints} from '@neos-project/neos-ui-backend-connector/src/Endpoints';
import {default as Endpoints, Routes} from '@neos-project/neos-ui-backend-connector/src/Endpoints';
type Endpoints = ReturnType<typeof Endpoints>;

export function * watchSyncing() {
yield takeEvery(actionTypes.CR.Syncing.STARTED, function * change() {
import {makeReloadNodes} from '../CR/NodeOperations/reloadNodes';

type SyncWorkspaceResult =
| { success: true }
| { conflicts: Conflict[] }
| { error: AnyError };

export function * watchSyncing({routes}: {routes: Routes}) {
const syncPersonalWorkspace = makeSyncPersonalWorkspace({routes});

yield takeEvery(actionTypes.CR.Syncing.STARTED, function * sync() {
if (yield * waitForConfirmation()) {
do {
yield * syncPersonalWorkspace(false);
Expand All @@ -45,46 +56,58 @@ function * waitForConfirmation() {
return Boolean(confirmed);
}

type SyncWorkspaceResult =
| { success: true }
| { conflicts: Conflict[] }
| { error: AnyError };

function * syncPersonalWorkspace(force: boolean) {
const {syncWorkspace} = backend.get().endpoints as Endpoints;
const personalWorkspaceName: WorkspaceName = yield select(selectors.CR.Workspaces.personalWorkspaceNameSelector);
const dimensionSpacePoint: null|DimensionCombination = yield select(selectors.CR.ContentDimensions.active);

try {
const result: SyncWorkspaceResult = yield call(syncWorkspace, personalWorkspaceName, force, dimensionSpacePoint);
if ('success' in result) {
yield put(actions.CR.Syncing.succeed());
} else if ('conflicts' in result) {
yield * resolveConflicts(result.conflicts);
} else {
yield put(actions.CR.Syncing.fail(result.error));
const makeSyncPersonalWorkspace = (deps: {
routes: Routes
}) => {
const refreshAfterSyncing = makeRefreshAfterSyncing(deps);
const resolveConflicts = makeResolveConflicts({syncPersonalWorkspace});

function * syncPersonalWorkspace(force: boolean) {
const {syncWorkspace} = backend.get().endpoints as Endpoints;
const personalWorkspaceName: WorkspaceName = yield select(selectors.CR.Workspaces.personalWorkspaceNameSelector);
const dimensionSpacePoint: null|DimensionCombination = yield select(selectors.CR.ContentDimensions.active);

try {
const result: SyncWorkspaceResult = yield call(syncWorkspace, personalWorkspaceName, force, dimensionSpacePoint);
if ('success' in result) {
yield * refreshAfterSyncing();
yield put(actions.CR.Syncing.succeed());
} else if ('conflicts' in result) {
yield * resolveConflicts(result.conflicts);
} else {
yield put(actions.CR.Syncing.fail(result.error));
}
} catch (error) {
yield put(actions.CR.Syncing.fail(error as AnyError));
}
} catch (error) {
yield put(actions.CR.Syncing.fail(error as AnyError));
} finally {
yield * refreshAfterSyncing();
}
}

function * resolveConflicts(conflicts: Conflict[]): any {
yield put(actions.CR.Syncing.resolve(conflicts));

const {payload: {strategy}}: ReturnType<
typeof actions.CR.Syncing.selectResolutionStrategy
> = yield take(actionTypes.CR.Syncing.RESOLUTION_STARTED);
return syncPersonalWorkspace;
}

if (yield * waitForResolutionConfirmation()) {
if (strategy === ResolutionStrategy.FORCE) {
yield * syncPersonalWorkspace(true);
} else if (strategy === ResolutionStrategy.DISCARD_ALL) {
yield * discardAll();
}
const makeResolveConflicts = (deps: {
syncPersonalWorkspace: ReturnType<typeof makeSyncPersonalWorkspace>
}) => {
const discardAll = makeDiscardAll(deps);

function * resolveConflicts(conflicts: Conflict[]): any {
yield put(actions.CR.Syncing.resolve(conflicts));

yield takeEvery<ReturnType<typeof actions.CR.Syncing.selectResolutionStrategy>>(
actionTypes.CR.Syncing.RESOLUTION_STARTED,
function * resolve({payload: {strategy}}) {
if (strategy === ResolutionStrategy.FORCE) {
if (yield * waitForResolutionConfirmation()) {
yield * deps.syncPersonalWorkspace(true);
}
} else if (strategy === ResolutionStrategy.DISCARD_ALL) {
yield * discardAll();
}
}
);
}

return resolveConflicts;
}

function * waitForResolutionConfirmation() {
Expand All @@ -111,14 +134,50 @@ function * waitForRetry() {
return Boolean(retried);
}

function * discardAll() {
yield console.log('@TODO: Discard All');
const makeDiscardAll = (deps: {
syncPersonalWorkspace: ReturnType<typeof makeSyncPersonalWorkspace>;
}) => {
function * discardAll() {
yield put(actions.CR.Publishing.start(
PublishingMode.DISCARD,
PublishingScope.ALL
));

const {cancelled, failed}: {
cancelled: null | ReturnType<typeof actions.CR.Publishing.cancel>;
failed: null | ReturnType<typeof actions.CR.Publishing.fail>;
finished: null | ReturnType<typeof actions.CR.Publishing.finish>;
} = yield race({
cancelled: take(actionTypes.CR.Publishing.CANCELLED),
failed: take(actionTypes.CR.Publishing.FAILED),
finished: take(actionTypes.CR.Publishing.FINISHED)
});

if (cancelled) {
yield put(actions.CR.Syncing.cancelResolution());
} else if (failed) {
yield put(actions.CR.Syncing.finish());
} else {
yield put(actions.CR.Syncing.confirmResolution());
yield * deps.syncPersonalWorkspace(false);
}
}

return discardAll;
}

function * refreshAfterSyncing() {
yield console.log('@TODO: Refresh after Sync');
// const {getWorkspaceInfo} = backend.get().endpoints as Endpoints;
// const workspaceInfo = yield call(getWorkspaceInfo);
// yield put(actions.CR.Workspaces.update(workspaceInfo));
// yield put(actions.UI.Remote.finishSynchronization());
const makeRefreshAfterSyncing = (deps: {
routes: Routes
}) => {
const {getWorkspaceInfo} = backend.get().endpoints as Endpoints;
const reloadNodes = makeReloadNodes(deps);

function * refreshAfterSyncing() {
const workspaceInfo: WorkspaceInformation = yield call(getWorkspaceInfo);
yield put(actions.CR.Workspaces.update(workspaceInfo));

yield * reloadNodes();
}

return refreshAfterSyncing;
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,15 @@ export const ResolutionStrategySelectionDialog: React.FC<{
workspaceName: WorkspaceName;
baseWorkspaceName: WorkspaceName;
conflicts: Conflict[];
defaultStrategy: null | ResolutionStrategy;
i18n: I18nRegistry;
onCancel: () => void;
onSelectResolutionStrategy: (strategy: ResolutionStrategy) => void;
}> = (props) => {
const [
selectedResolutionStrategy,
setSelectedResolutionStrategy
] = React.useState(ResolutionStrategy.FORCE);
] = React.useState(props.defaultStrategy ?? ResolutionStrategy.FORCE);
const options = React.useMemo(() => {
return OPTIONS_FOR_RESOLUTION_STRATEGY_SELECTION
.map(({value}) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,24 +117,28 @@ const SyncWorkspaceDialog: React.FC<SyncWorkspaceDialogProps> = (props) => {
workspaceName={props.personalWorkspaceName}
baseWorkspaceName={props.baseWorkspaceName}
conflicts={props.syncingState.process.conflicts}
defaultStrategy={props.syncingState.process.strategy}
i18n={props.i18nRegistry}
onCancel={handleCancel}
onSelectResolutionStrategy={handleSelectResolutionStrategy}
/>
);
case SyncingPhase.RESOLVING:
return (
<ResolutionStrategyConfirmationDialog
workspaceName={props.personalWorkspaceName}
baseWorkspaceName={props.baseWorkspaceName}
totalNumberOfChangesInWorkspace={props.totalNumberOfChangesInWorkspace}
strategy={props.syncingState.process.strategy}
conflicts={props.syncingState.process.conflicts}
i18n={props.i18nRegistry}
onCancelConflictResolution={handleCancelConflictResolution}
onConfirmResolutionStrategy={handleConfirmResolutionStrategy}
/>
);
if (props.syncingState.process.strategy === ResolutionStrategy.FORCE) {
return (
<ResolutionStrategyConfirmationDialog
workspaceName={props.personalWorkspaceName}
baseWorkspaceName={props.baseWorkspaceName}
totalNumberOfChangesInWorkspace={props.totalNumberOfChangesInWorkspace}
strategy={props.syncingState.process.strategy}
conflicts={props.syncingState.process.conflicts}
i18n={props.i18nRegistry}
onCancelConflictResolution={handleCancelConflictResolution}
onConfirmResolutionStrategy={handleConfirmResolutionStrategy}
/>
);
}
return null;
case SyncingPhase.ERROR:
case SyncingPhase.SUCCESS:
return (
Expand Down

0 comments on commit 5c51d02

Please sign in to comment.