diff --git a/packages/neos-ui-redux-store/src/CR/Publishing/index.ts b/packages/neos-ui-redux-store/src/CR/Publishing/index.ts index 21fea2cac6..7191709d60 100644 --- a/packages/neos-ui-redux-store/src/CR/Publishing/index.ts +++ b/packages/neos-ui-redux-store/src/CR/Publishing/index.ts @@ -25,6 +25,7 @@ export enum PublishingScope { export enum PublishingPhase { START, ONGOING, + CONFLICTS, SUCCESS, ERROR } @@ -35,6 +36,7 @@ export type State = null | { process: | { phase: PublishingPhase.START } | { phase: PublishingPhase.ONGOING } + | { phase: PublishingPhase.CONFLICTS } | { phase: PublishingPhase.ERROR; error: null | AnyError; @@ -51,6 +53,8 @@ export enum actionTypes { STARTED = '@neos/neos-ui/CR/Publishing/STARTED', CANCELLED = '@neos/neos-ui/CR/Publishing/CANCELLED', CONFIRMED = '@neos/neos-ui/CR/Publishing/CONFIRMED', + CONFLICTS_OCCURRED = '@neos/neos-ui/CR/Publishing/CONFLICTS_OCCURRED', + CONFLICTS_RESOLVED = '@neos/neos-ui/CR/Publishing/CONFLICTS_RESOLVED', FAILED = '@neos/neos-ui/CR/Publishing/FAILED', RETRIED = '@neos/neos-ui/CR/Publishing/RETRIED', SUCEEDED = '@neos/neos-ui/CR/Publishing/SUCEEDED', @@ -74,6 +78,16 @@ const cancel = () => createAction(actionTypes.CANCELLED); */ const confirm = () => createAction(actionTypes.CONFIRMED); +/** + * Signal that conflicts have occurred during the publish/discard operation + */ +const conflicts = () => createAction(actionTypes.CONFLICTS_OCCURRED); + +/** + * Signal that conflicts have been resolved during the publish/discard operation + */ +const resolveConflicts = () => createAction(actionTypes.CONFLICTS_RESOLVED); + /** * Signal that the ongoing publish/discard workflow has failed */ @@ -108,6 +122,8 @@ export const actions = { start, cancel, confirm, + conflicts, + resolveConflicts, fail, retry, succeed, @@ -145,6 +161,20 @@ export const reducer = (state: State = defaultState, action: Action): State => { phase: PublishingPhase.ONGOING } }; + case actionTypes.CONFLICTS_OCCURRED: + return { + ...state, + process: { + phase: PublishingPhase.CONFLICTS + } + }; + case actionTypes.CONFLICTS_RESOLVED: + return { + ...state, + process: { + phase: PublishingPhase.ONGOING + } + }; case actionTypes.FAILED: return { ...state, diff --git a/packages/neos-ui-redux-store/src/CR/Syncing/index.ts b/packages/neos-ui-redux-store/src/CR/Syncing/index.ts index f34d3c8e2e..ea6b159677 100644 --- a/packages/neos-ui-redux-store/src/CR/Syncing/index.ts +++ b/packages/neos-ui-redux-store/src/CR/Syncing/index.ts @@ -50,6 +50,7 @@ export type Conflict = { }; export type State = null | { + autoAcknowledge: boolean; process: | { phase: SyncingPhase.START } | { phase: SyncingPhase.ONGOING } @@ -178,12 +179,24 @@ export const reducer = (state: State = defaultState, action: Action): State => { if (state === null) { if (action.type === actionTypes.STARTED) { return { + autoAcknowledge: false, process: { phase: SyncingPhase.START } }; } + if (action.type === actionTypes.CONFLICTS_DETECTED) { + return { + autoAcknowledge: true, + process: { + phase: SyncingPhase.CONFLICT, + conflicts: action.payload.conflicts, + strategy: null + } + }; + } + return null; } @@ -192,12 +205,14 @@ export const reducer = (state: State = defaultState, action: Action): State => { return null; case actionTypes.CONFIRMED: return { + ...state, process: { phase: SyncingPhase.ONGOING } }; case actionTypes.CONFLICTS_DETECTED: return { + ...state, process: { phase: SyncingPhase.CONFLICT, conflicts: action.payload.conflicts, @@ -207,6 +222,7 @@ export const reducer = (state: State = defaultState, action: Action): State => { case actionTypes.RESOLUTION_STARTED: if (state.process.phase === SyncingPhase.CONFLICT) { return { + ...state, process: { ...state.process, phase: SyncingPhase.RESOLVING, @@ -218,6 +234,7 @@ export const reducer = (state: State = defaultState, action: Action): State => { case actionTypes.RESOLUTION_CANCELLED: if (state.process.phase === SyncingPhase.RESOLVING) { return { + ...state, process: { ...state.process, phase: SyncingPhase.CONFLICT @@ -227,12 +244,14 @@ export const reducer = (state: State = defaultState, action: Action): State => { return state; case actionTypes.RESOLUTION_CONFIRMED: return { + ...state, process: { phase: SyncingPhase.ONGOING } }; case actionTypes.FAILED: return { + ...state, process: { phase: SyncingPhase.ERROR, error: action.payload.error @@ -240,12 +259,18 @@ export const reducer = (state: State = defaultState, action: Action): State => { }; case actionTypes.RETRIED: return { + ...state, process: { phase: SyncingPhase.ONGOING } }; case actionTypes.SUCEEDED: + if (state.autoAcknowledge) { + return null; + } + return { + ...state, process: { phase: SyncingPhase.SUCCESS } diff --git a/packages/neos-ui-sagas/src/Publish/index.ts b/packages/neos-ui-sagas/src/Publish/index.ts index e552422347..ceb0673d5e 100644 --- a/packages/neos-ui-sagas/src/Publish/index.ts +++ b/packages/neos-ui-sagas/src/Publish/index.ts @@ -15,10 +15,12 @@ import {actionTypes, actions, selectors} from '@neos-project/neos-ui-redux-store import {GlobalState} from '@neos-project/neos-ui-redux-store/src/System'; import {FeedbackEnvelope} from '@neos-project/neos-ui-redux-store/src/ServerFeedback'; import {PublishingMode, PublishingScope} from '@neos-project/neos-ui-redux-store/src/CR/Publishing'; +import {Conflict} from '@neos-project/neos-ui-redux-store/src/CR/Syncing'; import backend, {Routes} from '@neos-project/neos-ui-backend-connector'; import {makeReloadNodes} from '../CR/NodeOperations/reloadNodes'; import {updateWorkspaceInfo} from '../CR/Workspaces'; +import {makeResolveConflicts, makeSyncPersonalWorkspace} from '../Sync'; const handleWindowBeforeUnload = (event: BeforeUnloadEvent) => { event.preventDefault(); @@ -32,6 +34,7 @@ type PublishingResponse = numberOfAffectedChanges: number; } } + | { conflicts: Conflict[] } | { error: AnyError }; export function * watchPublishing({routes}: {routes: Routes}) { @@ -67,6 +70,8 @@ export function * watchPublishing({routes}: {routes: Routes}) { }; const reloadAfterPublishing = makeReloadAfterPublishing({routes}); + const syncPersonalWorkspace = makeSyncPersonalWorkspace({routes}); + const resolveConflicts = makeResolveConflicts({syncPersonalWorkspace}); yield takeEvery(actionTypes.CR.Publishing.STARTED, function * publishingWorkflow(action: ReturnType) { const confirmed = yield * waitForConfirmation(); @@ -88,21 +93,37 @@ export function * watchPublishing({routes}: {routes: Routes}) { ? yield select(ancestorIdSelector) : null; + function * attemptToPublishOrDiscard(): Generator { + const result: PublishingResponse = scope === PublishingScope.ALL + ? yield call(endpoint as any, workspaceName) + : yield call(endpoint!, ancestorId, workspaceName, dimensionSpacePoint); + + if ('success' in result) { + yield put(actions.CR.Publishing.succeed(result.success.numberOfAffectedChanges)); + yield * reloadAfterPublishing(); + } else if ('conflicts' in result) { + yield put(actions.CR.Publishing.conflicts()); + const conflictsWereResolved: boolean = + yield * resolveConflicts(result.conflicts); + + if (conflictsWereResolved) { + yield put(actions.CR.Publishing.resolveConflicts()); + yield * attemptToPublishOrDiscard(); + } else { + yield put(actions.CR.Publishing.cancel()); + yield call(updateWorkspaceInfo); + } + } else if ('error' in result) { + yield put(actions.CR.Publishing.fail(result.error)); + } else { + yield put(actions.CR.Publishing.fail(null)); + } + } + do { try { window.addEventListener('beforeunload', handleWindowBeforeUnload); - const result: PublishingResponse = scope === PublishingScope.ALL - ? yield call(endpoint as any, workspaceName) - : yield call(endpoint, ancestorId, workspaceName, dimensionSpacePoint); - - if ('success' in result) { - yield put(actions.CR.Publishing.succeed(result.success.numberOfAffectedChanges)); - yield * reloadAfterPublishing(); - } else if ('error' in result) { - yield put(actions.CR.Publishing.fail(result.error)); - } else { - yield put(actions.CR.Publishing.fail(null)); - } + yield * attemptToPublishOrDiscard(); } catch (error) { yield put(actions.CR.Publishing.fail(error as AnyError)); } finally { @@ -127,6 +148,13 @@ function * waitForConfirmation() { } function * waitForRetry() { + const isOngoing: boolean = yield select( + (state: GlobalState) => state.cr.publishing !== null + ); + if (!isOngoing) { + return false; + } + const {retried}: { acknowledged: ReturnType; retried: ReturnType; diff --git a/packages/neos-ui-sagas/src/Sync/index.ts b/packages/neos-ui-sagas/src/Sync/index.ts index b6270a5f10..7dbc8054ec 100644 --- a/packages/neos-ui-sagas/src/Sync/index.ts +++ b/packages/neos-ui-sagas/src/Sync/index.ts @@ -62,7 +62,7 @@ function * waitForConfirmation() { return Boolean(confirmed); } -const makeSyncPersonalWorkspace = (deps: { +export const makeSyncPersonalWorkspace = (deps: { routes: Routes }) => { const refreshAfterSyncing = makeRefreshAfterSyncing(deps); @@ -94,7 +94,7 @@ const makeSyncPersonalWorkspace = (deps: { return syncPersonalWorkspace; } -const makeResolveConflicts = (deps: { +export const makeResolveConflicts = (deps: { syncPersonalWorkspace: ReturnType }) => { const discardAll = makeDiscardAll(deps); @@ -102,18 +102,33 @@ const makeResolveConflicts = (deps: { function * resolveConflicts(conflicts: Conflict[]): any { yield put(actions.CR.Syncing.resolve(conflicts)); - yield takeEvery>( - 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(); + const {started}: { + cancelled: null | ReturnType; + started: null | ReturnType; + } = yield race({ + cancelled: take(actionTypes.CR.Syncing.CANCELLED), + started: take(actionTypes.CR.Syncing.RESOLUTION_STARTED) + }); + + if (started) { + const {payload: {strategy}} = started; + + if (strategy === ResolutionStrategy.FORCE) { + if (yield * waitForResolutionConfirmation()) { + yield * deps.syncPersonalWorkspace(true); + return true; } + + return false; } - ); + + if (strategy === ResolutionStrategy.DISCARD_ALL) { + yield * discardAll(); + return true; + } + } + + return false; } return resolveConflicts; diff --git a/packages/neos-ui/src/Containers/Modals/PublishingDialog/PublishingDialog.tsx b/packages/neos-ui/src/Containers/Modals/PublishingDialog/PublishingDialog.tsx index b8844bfd4d..7997c5916f 100644 --- a/packages/neos-ui/src/Containers/Modals/PublishingDialog/PublishingDialog.tsx +++ b/packages/neos-ui/src/Containers/Modals/PublishingDialog/PublishingDialog.tsx @@ -96,6 +96,9 @@ const PublishingDialog: React.FC = (props) => { /> ); + case PublishingPhase.CONFLICTS: + return null; + case PublishingPhase.ERROR: case PublishingPhase.SUCCESS: return (