From 569d4bdf851120ed414ae02bf31e6dbfd92a3cb7 Mon Sep 17 00:00:00 2001 From: Wilhelm Behncke Date: Mon, 8 Apr 2024 17:34:55 +0200 Subject: [PATCH] TASK: Implement reloadNodes saga using the ReloadNodes query endpoint --- .../src/Endpoints/index.ts | 25 +++- .../src/CR/NodeOperations/index.js | 4 +- .../src/CR/NodeOperations/reloadNodes.ts | 127 ++++++++++++++++++ 3 files changed, 154 insertions(+), 2 deletions(-) create mode 100644 packages/neos-ui-sagas/src/CR/NodeOperations/reloadNodes.ts diff --git a/packages/neos-ui-backend-connector/src/Endpoints/index.ts b/packages/neos-ui-backend-connector/src/Endpoints/index.ts index 68d8a6f221..1bcfed2c08 100644 --- a/packages/neos-ui-backend-connector/src/Endpoints/index.ts +++ b/packages/neos-ui-backend-connector/src/Endpoints/index.ts @@ -23,6 +23,7 @@ export interface Routes { generateUriPathSegment: string; getWorkspaceInfo: string; getAdditionalNodeMetadata: string; + reloadNodes: string; }; }; core: { @@ -694,6 +695,27 @@ export default (routes: Routes) => { .then(response => fetchWithErrorHandling.parseJson(response)) .catch(reason => fetchWithErrorHandling.generalErrorHandler(reason)); + const reloadNodes = (query: { + workspaceName: WorkspaceName; + dimensionSpacePoint: DimensionCombination; + siteId: NodeContextPath; + documentId: NodeContextPath; + ancestorsOfDocumentIds: NodeContextPath[]; + toggledNodesIds: NodeContextPath[]; + clipboardNodesIds: NodeContextPath[]; + }) => fetchWithErrorHandling.withCsrfToken(csrfToken => ({ + url: routes.ui.service.reloadNodes, + method: 'POST', + credentials: 'include', + headers: { + 'X-Flow-Csrftoken': csrfToken, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({query}) + })) + .then(response => fetchWithErrorHandling.parseJson(response)) + .catch(reason => fetchWithErrorHandling.generalErrorHandler(reason)); + return { loadImageMetadata, change, @@ -726,6 +748,7 @@ export default (routes: Routes) => { tryLogin, contentDimensions, impersonateStatus, - impersonateRestore + impersonateRestore, + reloadNodes }; }; diff --git a/packages/neos-ui-sagas/src/CR/NodeOperations/index.js b/packages/neos-ui-sagas/src/CR/NodeOperations/index.js index c3e5151dc7..cfca6e599f 100644 --- a/packages/neos-ui-sagas/src/CR/NodeOperations/index.js +++ b/packages/neos-ui-sagas/src/CR/NodeOperations/index.js @@ -6,6 +6,7 @@ import moveDroppedNodes from './moveDroppedNodes'; import hideNode from './hideNode'; import showNode from './showNode'; import reloadState from './reloadState'; +import {makeReloadNodes} from './reloadNodes'; export { addNode, @@ -15,5 +16,6 @@ export { moveDroppedNodes, hideNode, showNode, - reloadState + reloadState, + makeReloadNodes }; diff --git a/packages/neos-ui-sagas/src/CR/NodeOperations/reloadNodes.ts b/packages/neos-ui-sagas/src/CR/NodeOperations/reloadNodes.ts new file mode 100644 index 0000000000..87b4601c99 --- /dev/null +++ b/packages/neos-ui-sagas/src/CR/NodeOperations/reloadNodes.ts @@ -0,0 +1,127 @@ +/* + * 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. + */ +import {call, put, select} from 'redux-saga/effects'; + +import {actions, selectors} from '@neos-project/neos-ui-redux-store'; +import {GlobalState} from '@neos-project/neos-ui-redux-store/src/System'; +import {DimensionCombination, Node, NodeContextPath, NodeMap, WorkspaceName} from '@neos-project/neos-ts-interfaces'; +import {AnyError} from '@neos-project/neos-ui-error'; +import backend from '@neos-project/neos-ui-backend-connector'; +// @ts-ignore +import {getGuestFrameDocument} from '@neos-project/neos-ui-guest-frame/src/dom'; + +// @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, Routes} from '@neos-project/neos-ui-backend-connector/src/Endpoints'; +type Endpoints = ReturnType; + +type ReloadNodesResponse = + | { + success: { + documentId: NodeContextPath; + nodes: NodeMap; + } + } + | {error: AnyError} + +export const makeReloadNodes = (deps: { + routes?: Routes; +}) => { + const redirectToDefaultModule = makeRedirectToDefaultModule(deps); + const {reloadNodes: reloadNodesEndpoint} = backend.get().endpoints as Endpoints; + + return function * reloadNodes() { + const workspaceName: WorkspaceName = yield select( + (state) => state.cr.workspaces.personalWorkspace.name + ); + const dimensionSpacePoint: DimensionCombination = yield select( + (state) => state.cr.contentDimensions.active + ); + const {siteNode: siteId, documentNode: documentId}: GlobalState['cr']['nodes'] = + yield select((state) => state.cr.nodes); + if (!siteId || !documentId) { + redirectToDefaultModule(); + return; + } + + const documentNodeParentLine: ReturnType< + typeof selectors.CR.Nodes.documentNodeParentLineSelector + > = yield select(selectors.CR.Nodes.documentNodeParentLineSelector); + const ancestorsOfDocumentIds = documentNodeParentLine + .map((nodeOrNull) => nodeOrNull?.contextPath) + .filter((nodeIdOrNull) => Boolean(nodeIdOrNull)) as NodeContextPath[]; + const clipboardNodesIds: NodeContextPath[] = yield select( + selectors.CR.Nodes.clipboardNodesContextPathsSelector + ); + const toggledNodesIds: NodeContextPath[] = yield select( + state => state?.ui?.pageTree?.toggled + ); + + yield put(actions.UI.PageTree.setAsLoading(siteId)); + + const result: ReloadNodesResponse = yield call(reloadNodesEndpoint, { + workspaceName, + dimensionSpacePoint, + siteId, + documentId, + ancestorsOfDocumentIds, + clipboardNodesIds, + toggledNodesIds + }); + + if ('success' in result) { + yield put(actions.CR.Nodes.setState({ + siteNodeContextPath: siteId, + documentNodeContextPath: result.success.documentId, + nodes: result.success.nodes, + merge: false + })); + + yield put(actions.UI.PageTree.setAsLoaded(siteId)); + + if (documentId === result.success.documentId) { + // If the document is still available, reload the guest frame + getGuestFrameDocument().location.reload(); + } else { + // If it's gone try to navigate to the next available ancestor document + const availableAncestorDocumentNode: Node = yield select( + selectors.CR.Nodes.byContextPathSelector(result.success.documentId) + ); + + if (availableAncestorDocumentNode) { + yield put(actions.UI.ContentCanvas.setSrc(availableAncestorDocumentNode.uri)); + } else { + // We're doomed - there's no document left to navigate to + // In this (rather unlikely) case, we leave the UI and navigate + // to whatever default entry module is configured: + redirectToDefaultModule(); + } + } + } else { + // If reloading failed on the server side, one of three things happened: + // 1. No document node could be found + // 2. No site node could be found + // 3. Some other, more profound error occurred + // + // No matter which scenario, we'd end up in an invalid UI state. This is + // why we need to escape to whatever default entry module is configured: + redirectToDefaultModule(); + } + }; +} + +const makeRedirectToDefaultModule = (deps: { + routes?: Routes; +}) => { + return function redirectToDefaultModule() { + window.location.href = deps.routes?.core?.modules?.defaultModule!; + }; +};