From b56135a01ecf59ae3a4990e3fd54ac766732e0e6 Mon Sep 17 00:00:00 2001 From: Sebastian Helzle Date: Wed, 24 Apr 2024 11:31:08 +0200 Subject: [PATCH 01/13] BUGFIX: Improve loading performance of UI and remove script tag with nodedata With this change the minimal required nodedata for each node in the rendered content is inserted as data attribute and not as inline script anymore. This improves performance as no extra function call is executed for each node. Additonally the reduction in rendered node attributes reduces the output filesize and again improves loading time. To prevent just-in-time loading of nodes all incomplete nodedata is requested after the guest frame has finished loading. --- Classes/Aspects/AugmentationAspect.php | 5 +- .../src/initializeGuestFrame.js | 52 +++++++++++++++---- 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/Classes/Aspects/AugmentationAspect.php b/Classes/Aspects/AugmentationAspect.php index f57ed7d88c..ea107628b2 100644 --- a/Classes/Aspects/AugmentationAspect.php +++ b/Classes/Aspects/AugmentationAspect.php @@ -128,13 +128,12 @@ public function contentElementAugmentation(JoinPointInterface $joinPoint) $this->userLocaleService->switchToUILocale(); - $serializedNode = json_encode($this->nodeInfoHelper->renderNodeWithPropertiesAndChildrenInformation($node, $this->controllerContext)); + $serializedNode = json_encode($this->nodeInfoHelper->renderNodeWithMinimalPropertiesAndChildrenInformation($node, $this->controllerContext)); + $attributes['data-__neos-nodedata'] = $serializedNode; $this->userLocaleService->switchToUILocale(true); $wrappedContent = $this->htmlAugmenter->addAttributes($content, $attributes, 'div'); - $wrappedContent .= ""; - return $wrappedContent; } diff --git a/packages/neos-ui-guest-frame/src/initializeGuestFrame.js b/packages/neos-ui-guest-frame/src/initializeGuestFrame.js index 0fa946f957..ed897c3b9d 100644 --- a/packages/neos-ui-guest-frame/src/initializeGuestFrame.js +++ b/packages/neos-ui-guest-frame/src/initializeGuestFrame.js @@ -1,21 +1,22 @@ -import {takeEvery, put, select} from 'redux-saga/effects'; +import {put, select, takeEvery} from 'redux-saga/effects'; import {$get} from 'plow-js'; -import {selectors, actions, actionTypes} from '@neos-project/neos-ui-redux-store'; +import {actions, actionTypes, selectors} from '@neos-project/neos-ui-redux-store'; import {requestIdleCallback} from '@neos-project/utils-helpers'; import initializeContentDomNode from './initializeContentDomNode'; import { - getGuestFrameWindow, - getGuestFrameDocument, + dispatchCustomEvent, findAllNodesInGuestFrame, findInGuestFrame, findNodeInGuestFrame, - dispatchCustomEvent + getGuestFrameDocument, + getGuestFrameWindow } from './dom'; import style from './style.module.css'; import {SelectionModeTypes} from '@neos-project/neos-ts-interfaces'; +import backend from "@neos-project/neos-ui-backend-connector"; // // Get all parent elements of the event target. @@ -60,13 +61,36 @@ export default ({globalRegistry, store}) => function * initializeGuestFrame() { return; } - const nodes = Object.assign({}, guestFrameWindow['@Neos.Neos.Ui:Nodes'], { - [documentInformation.metaData.documentNode]: documentInformation.metaData.documentNodeSerialization - }); + // Load legacy node data scripts from guest frame - remove with Neos 9.0 + const legacyNodeData = guestFrameWindow['@Neos.Neos.Ui:NodeData'] || {}; + + // Load nodedata from augmented nodes in guest frame + const embeddedNodeData = {}; + Array.prototype.forEach.call( + guestFrameWindow.document.querySelectorAll('[data-__neos-nodedata]'), + (element) => { + const contextPath = element.dataset.__neosNodeContextpath; + try { + embeddedNodeData[contextPath] = JSON.parse(element.dataset.__neosNodedata); + element.removeAttribute('data-__neos-nodedata'); + } catch (e) { + console.error('Could not parse node data for context path', contextPath, e); + } + } + ); + + const nodes = Object.assign( + {}, + legacyNodeData, // Merge legacy node data from the guest frame - remove with Neos 9.0 + embeddedNodeData, + { + [documentInformation.metaData.documentNode]: documentInformation.metaData.documentNodeSerialization + } + ); yield put(actions.CR.Nodes.merge(nodes)); - // Remove the inline scripts after initialization + // Remove the legacy inline scripts after initialization - remove with Neos 9.0 Array.prototype.forEach.call(guestFrameWindow.document.querySelectorAll('script[data-neos-nodedata]'), element => element.parentElement.removeChild(element)); const state = store.getState(); @@ -210,4 +234,14 @@ export default ({globalRegistry, store}) => function * initializeGuestFrame() { node.classList.remove(style['markActiveNodeAsFocused--focusedNode']); } }); + + // Load all nodedata from content at the end of the initialisation to avoid blocking the UI + const {q} = backend.get(); + const fullyLoadedNodesInContent = yield q(Object.keys(embeddedNodeData)).get(); + if (fullyLoadedNodesInContent) { + yield put(actions.CR.Nodes.merge(fullyLoadedNodesInContent.reduce((nodes, node) => { + nodes[node.contextPath] = node; + return nodes; + }, {}))); + } }; From 9d41d580b87bb5701648103426f07b82b97f3f3f Mon Sep 17 00:00:00 2001 From: Sebastian Helzle Date: Wed, 24 Apr 2024 11:31:17 +0200 Subject: [PATCH 02/13] TASK: Cache user locale --- Classes/Domain/Service/UserLocaleService.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Classes/Domain/Service/UserLocaleService.php b/Classes/Domain/Service/UserLocaleService.php index b7dc351b24..e4451b4369 100644 --- a/Classes/Domain/Service/UserLocaleService.php +++ b/Classes/Domain/Service/UserLocaleService.php @@ -37,6 +37,13 @@ class UserLocaleService */ protected $rememberedContentLocale; + /** + * Remebered content locale for locale switching + * + * @var Locale + */ + protected $rememberedUserLocale; + /** * For serialization, we need to respect the UI locale, rather than the content locale * @@ -47,11 +54,15 @@ public function switchToUILocale($reset = false) if ($reset === true) { // Reset the locale $this->i18nService->getConfiguration()->setCurrentLocale($this->rememberedContentLocale); + } elseif ($this->rememberedUserLocale) { + // Restore the local + $this->i18nService->getConfiguration()->setCurrentLocale($this->rememberedUserLocale); } else { $this->rememberedContentLocale = $this->i18nService->getConfiguration()->getCurrentLocale(); $userLocalePreference = ($this->userService->getCurrentUser() ? $this->userService->getCurrentUser()->getPreferences()->getInterfaceLanguage() : null); $defaultLocale = $this->i18nService->getConfiguration()->getDefaultLocale(); $userLocale = $userLocalePreference ? new Locale($userLocalePreference) : $defaultLocale; + $this->rememberedUserLocale = $userLocale; $this->i18nService->getConfiguration()->setCurrentLocale($userLocale); } } From 397fd51698ea266045d0c9260620ec4ae1d12504 Mon Sep 17 00:00:00 2001 From: Sebastian Helzle Date: Wed, 24 Apr 2024 15:36:02 +0200 Subject: [PATCH 03/13] =?UTF-8?q?TASK:=20Don=E2=80=99t=20render=20nodedata?= =?UTF-8?q?=20to=20content=20and=20just=20load=20on=20initialisation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Classes/Aspects/AugmentationAspect.php | 22 +----------- .../src/initializeGuestFrame.js | 35 +++++-------------- 2 files changed, 10 insertions(+), 47 deletions(-) diff --git a/Classes/Aspects/AugmentationAspect.php b/Classes/Aspects/AugmentationAspect.php index ea107628b2..281e812d1f 100644 --- a/Classes/Aspects/AugmentationAspect.php +++ b/Classes/Aspects/AugmentationAspect.php @@ -38,24 +38,12 @@ class AugmentationAspect */ protected $nodeAuthorizationService; - /** - * @Flow\Inject - * @var UserLocaleService - */ - protected $userLocaleService; - /** * @Flow\Inject * @var HtmlAugmenter */ protected $htmlAugmenter; - /** - * @Flow\Inject - * @var NodeInfoHelper - */ - protected $nodeInfoHelper; - /** * @Flow\Inject * @var SessionInterface @@ -126,15 +114,7 @@ public function contentElementAugmentation(JoinPointInterface $joinPoint) $attributes['data-__neos-node-contextpath'] = $node->getContextPath(); $attributes['data-__neos-fusion-path'] = $fusionPath; - $this->userLocaleService->switchToUILocale(); - - $serializedNode = json_encode($this->nodeInfoHelper->renderNodeWithMinimalPropertiesAndChildrenInformation($node, $this->controllerContext)); - $attributes['data-__neos-nodedata'] = $serializedNode; - - $this->userLocaleService->switchToUILocale(true); - - $wrappedContent = $this->htmlAugmenter->addAttributes($content, $attributes, 'div'); - return $wrappedContent; + return $this->htmlAugmenter->addAttributes($content, $attributes); } /** diff --git a/packages/neos-ui-guest-frame/src/initializeGuestFrame.js b/packages/neos-ui-guest-frame/src/initializeGuestFrame.js index ed897c3b9d..777426c75c 100644 --- a/packages/neos-ui-guest-frame/src/initializeGuestFrame.js +++ b/packages/neos-ui-guest-frame/src/initializeGuestFrame.js @@ -16,7 +16,7 @@ import { import style from './style.module.css'; import {SelectionModeTypes} from '@neos-project/neos-ts-interfaces'; -import backend from "@neos-project/neos-ui-backend-connector"; +import backend from '@neos-project/neos-ui-backend-connector'; // // Get all parent elements of the event target. @@ -64,25 +64,18 @@ export default ({globalRegistry, store}) => function * initializeGuestFrame() { // Load legacy node data scripts from guest frame - remove with Neos 9.0 const legacyNodeData = guestFrameWindow['@Neos.Neos.Ui:NodeData'] || {}; - // Load nodedata from augmented nodes in guest frame - const embeddedNodeData = {}; - Array.prototype.forEach.call( - guestFrameWindow.document.querySelectorAll('[data-__neos-nodedata]'), - (element) => { - const contextPath = element.dataset.__neosNodeContextpath; - try { - embeddedNodeData[contextPath] = JSON.parse(element.dataset.__neosNodedata); - element.removeAttribute('data-__neos-nodedata'); - } catch (e) { - console.error('Could not parse node data for context path', contextPath, e); - } - } - ); + // Load all nodedata for nodes in the guest frame + const {q} = yield backend.get(); + const nodeContextPathsInGuestFrame = findAllNodesInGuestFrame().map(node => node.getAttribute('data-__neos-node-contextpath')); + const fullyLoadedNodesFromContent = (yield q(nodeContextPathsInGuestFrame).get()).reduce((nodes, node) => { + nodes[node.contextPath] = node; + return nodes; + }, {}); const nodes = Object.assign( {}, legacyNodeData, // Merge legacy node data from the guest frame - remove with Neos 9.0 - embeddedNodeData, + fullyLoadedNodesFromContent, { [documentInformation.metaData.documentNode]: documentInformation.metaData.documentNodeSerialization } @@ -234,14 +227,4 @@ export default ({globalRegistry, store}) => function * initializeGuestFrame() { node.classList.remove(style['markActiveNodeAsFocused--focusedNode']); } }); - - // Load all nodedata from content at the end of the initialisation to avoid blocking the UI - const {q} = backend.get(); - const fullyLoadedNodesInContent = yield q(Object.keys(embeddedNodeData)).get(); - if (fullyLoadedNodesInContent) { - yield put(actions.CR.Nodes.merge(fullyLoadedNodesInContent.reduce((nodes, node) => { - nodes[node.contextPath] = node; - return nodes; - }, {}))); - } }; From c3edb04800af8f438fa99c1cf07119dad19b2790 Mon Sep 17 00:00:00 2001 From: Sebastian Helzle Date: Mon, 6 May 2024 16:17:38 +0200 Subject: [PATCH 04/13] TASK: Optimise node context resolution from flow query --- .../ContentRepository/Service/NodeService.php | 69 +++++++++++-------- Classes/Fusion/Helper/NodeInfoHelper.php | 8 +-- 2 files changed, 45 insertions(+), 32 deletions(-) diff --git a/Classes/ContentRepository/Service/NodeService.php b/Classes/ContentRepository/Service/NodeService.php index 0e725bb277..49be79d31e 100644 --- a/Classes/ContentRepository/Service/NodeService.php +++ b/Classes/ContentRepository/Service/NodeService.php @@ -13,6 +13,7 @@ use Neos\ContentRepository\Domain\Model\NodeInterface; use Neos\ContentRepository\Domain\Model\Workspace; +use Neos\ContentRepository\Domain\Service\Context; use Neos\ContentRepository\Domain\Service\ContextFactoryInterface; use Neos\ContentRepository\Domain\Utility\NodePaths; use Neos\Eel\FlowQuery\FlowQuery; @@ -46,6 +47,11 @@ class NodeService */ protected $domainRepository; + /** + * @var array + */ + protected array $contextCache = []; + /** * Helper method to retrieve the closest document for a node * @@ -86,36 +92,43 @@ public function getNodeFromContextPath($contextPath, Site $site = null, Domain $ $nodePath = $nodePathAndContext['nodePath']; $workspaceName = $nodePathAndContext['workspaceName']; $dimensions = $nodePathAndContext['dimensions']; + $siteNodeName = $site ? $site->getNodeName() : explode('/', $nodePath)[2]; - $contextProperties = $this->prepareContextProperties($workspaceName, $dimensions); - - if ($site === null) { - list(, , $siteNodeName) = explode('/', $nodePath); - $site = $this->siteRepository->findOneByNodeName($siteNodeName); - } - - if ($domain === null) { - $domain = $this->domainRepository->findOneBySite($site); - } - - $contextProperties['currentSite'] = $site; - $contextProperties['currentDomain'] = $domain; - if ($includeAll === true) { - $contextProperties['invisibleContentShown'] = true; - $contextProperties['removedContentShown'] = true; - $contextProperties['inaccessibleContentShown'] = true; - } - - $context = $this->contextFactory->create( - $contextProperties - ); - - $workspace = $context->getWorkspace(false); - if (!$workspace) { - return new Error( - sprintf('Could not convert the given source to Node object because the workspace "%s" as specified in the context node path does not exist.', $workspaceName), - 1451392329 + // Prevent reloading the same context multiple times + $contextHash = md5(implode('|', [$siteNodeName, $workspaceName, json_encode($dimensions), $includeAll])); + if (isset($this->contextCache[$contextHash])) { + $context = $this->contextCache[$contextHash]; + } else { + $contextProperties = $this->prepareContextProperties($workspaceName, $dimensions); + + if ($site === null) { + $site = $this->siteRepository->findOneByNodeName($siteNodeName); + } + + if ($domain === null) { + $domain = $this->domainRepository->findOneBySite($site); + } + + $contextProperties['currentSite'] = $site; + $contextProperties['currentDomain'] = $domain; + if ($includeAll === true) { + $contextProperties['invisibleContentShown'] = true; + $contextProperties['removedContentShown'] = true; + $contextProperties['inaccessibleContentShown'] = true; + } + + $context = $this->contextFactory->create( + $contextProperties ); + + $workspace = $context->getWorkspace(false); + if (!$workspace) { + return new Error( + sprintf('Could not convert the given source to Node object because the workspace "%s" as specified in the context node path does not exist.', $workspaceName), + 1451392329 + ); + } + $this->contextCache[$contextHash] = $context; } return $context->getNode($nodePath); diff --git a/Classes/Fusion/Helper/NodeInfoHelper.php b/Classes/Fusion/Helper/NodeInfoHelper.php index a045697ae0..ba8122a402 100644 --- a/Classes/Fusion/Helper/NodeInfoHelper.php +++ b/Classes/Fusion/Helper/NodeInfoHelper.php @@ -155,7 +155,7 @@ public function renderNodeWithMinimalPropertiesAndChildrenInformation(NodeInterf /** * @param NodeInterface $node * @param ControllerContext|null $controllerContext - * @param string $nodeTypeFilterOverride + * @param string|null $nodeTypeFilterOverride * @return array|null */ public function renderNodeWithPropertiesAndChildrenInformation(NodeInterface $node, ControllerContext $controllerContext = null, string $nodeTypeFilterOverride = null) @@ -177,7 +177,7 @@ public function renderNodeWithPropertiesAndChildrenInformation(NodeInterface $no } } - $baseNodeType = $nodeTypeFilterOverride ? $nodeTypeFilterOverride : (isset($presetBaseNodeType) ? $presetBaseNodeType : $this->defaultBaseNodeType); + $baseNodeType = $nodeTypeFilterOverride ?: (isset($presetBaseNodeType) ? $presetBaseNodeType : $this->defaultBaseNodeType); $nodeInfo['children'] = $this->renderChildrenInformation($node, $baseNodeType); $this->userLocaleService->switchToUILocale(true); @@ -246,10 +246,10 @@ protected function renderChildrenInformation(NodeInterface $node, string $nodeTy $contentChildNodes = $node->getChildNodes($this->buildContentChildNodeFilterString()); $childNodes = array_merge($documentChildNodes, $contentChildNodes); - $mapper = function (NodeInterface $childNode) { + $mapper = static function (NodeInterface $childNode) { return [ 'contextPath' => $childNode->getContextPath(), - 'nodeType' => $childNode->getNodeType()->getName() // TODO: DUPLICATED; should NOT be needed!!! + 'nodeType' => $childNode->getNodeType()->getName() ]; }; From d7a81e1a79da8552feb1226060b5bef08fd5aa95 Mon Sep 17 00:00:00 2001 From: Sebastian Helzle Date: Thu, 13 Jun 2024 14:12:33 +0200 Subject: [PATCH 05/13] Update packages/neos-ui-guest-frame/src/initializeGuestFrame.js Co-authored-by: Marc Henry Schultz <85400359+mhsdesign@users.noreply.github.com> --- packages/neos-ui-guest-frame/src/initializeGuestFrame.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/neos-ui-guest-frame/src/initializeGuestFrame.js b/packages/neos-ui-guest-frame/src/initializeGuestFrame.js index 777426c75c..f3075894f5 100644 --- a/packages/neos-ui-guest-frame/src/initializeGuestFrame.js +++ b/packages/neos-ui-guest-frame/src/initializeGuestFrame.js @@ -62,7 +62,7 @@ export default ({globalRegistry, store}) => function * initializeGuestFrame() { } // Load legacy node data scripts from guest frame - remove with Neos 9.0 - const legacyNodeData = guestFrameWindow['@Neos.Neos.Ui:NodeData'] || {}; + const legacyNodeData = guestFrameWindow['@Neos.Neos.Ui:Nodes'] || {}; // Load all nodedata for nodes in the guest frame const {q} = yield backend.get(); From 43a9b0787b90d59fe0809fc1f42edcd2db33f218 Mon Sep 17 00:00:00 2001 From: Sebastian Helzle Date: Tue, 18 Jun 2024 17:14:25 +0200 Subject: [PATCH 06/13] TASK: Load full nodes from content tree and skip them from loading for guest frame The content tree is loaded early and can load fully loaded nodes without being slower as the response time is the same. But this way we can skip lots of nodes from being loaded by the guest frame if they are already present in the store. --- .../Controller/BackendServiceController.php | 17 ++++++++++++++--- .../src/initializeGuestFrame.js | 18 +++++++++++++++++- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/Classes/Controller/BackendServiceController.php b/Classes/Controller/BackendServiceController.php index 584128f25f..7967129d60 100644 --- a/Classes/Controller/BackendServiceController.php +++ b/Classes/Controller/BackendServiceController.php @@ -548,14 +548,25 @@ function ($envelope) { switch ($finisher['type']) { case 'get': - $result = $nodeInfoHelper->renderNodes(array_filter($flowQuery->get()), $this->getControllerContext()); + $result = $nodeInfoHelper->renderNodes(array_filter( + $flowQuery->get()), + $this->getControllerContext() + ); break; case 'getForTree': - $result = $nodeInfoHelper->renderNodes(array_filter($flowQuery->get()), $this->getControllerContext(), true); + $result = $nodeInfoHelper->renderNodes( + array_filter($flowQuery->get()), + $this->getControllerContext(), + true + ); break; case 'getForTreeWithParents': $nodeTypeFilter = $finisher['payload']['nodeTypeFilter'] ?? null; - $result = $nodeInfoHelper->renderNodesWithParents(array_filter($flowQuery->get()), $this->getControllerContext(), $nodeTypeFilter); + $result = $nodeInfoHelper->renderNodesWithParents( + array_filter($flowQuery->get()), + $this->getControllerContext(), + $nodeTypeFilter + ); break; } diff --git a/packages/neos-ui-guest-frame/src/initializeGuestFrame.js b/packages/neos-ui-guest-frame/src/initializeGuestFrame.js index f3075894f5..907b0b73df 100644 --- a/packages/neos-ui-guest-frame/src/initializeGuestFrame.js +++ b/packages/neos-ui-guest-frame/src/initializeGuestFrame.js @@ -67,7 +67,22 @@ export default ({globalRegistry, store}) => function * initializeGuestFrame() { // Load all nodedata for nodes in the guest frame const {q} = yield backend.get(); const nodeContextPathsInGuestFrame = findAllNodesInGuestFrame().map(node => node.getAttribute('data-__neos-node-contextpath')); - const fullyLoadedNodesFromContent = (yield q(nodeContextPathsInGuestFrame).get()).reduce((nodes, node) => { + + // Filter nodes that are already loaded in the redux store + const nodesByContextPath = store.getState().cr.nodes.byContextPath; + const nodesAlreadyPresentInStore = {}; + const notFullyLoadedNodeContextPaths = nodeContextPathsInGuestFrame.filter((contextPath) => { + const node = nodesByContextPath[contextPath]; + const nodeIsLoaded = node !== undefined && node.isFullyLoaded; + if (nodeIsLoaded){ + nodesAlreadyPresentInStore[contextPath] = node; + return false; + } + return true; + }); + + // Load remaining list of nodes from the backend + const fullyLoadedNodesFromContent = (yield q(notFullyLoadedNodeContextPaths).get()).reduce((nodes, node) => { nodes[node.contextPath] = node; return nodes; }, {}); @@ -75,6 +90,7 @@ export default ({globalRegistry, store}) => function * initializeGuestFrame() { const nodes = Object.assign( {}, legacyNodeData, // Merge legacy node data from the guest frame - remove with Neos 9.0 + nodesAlreadyPresentInStore, fullyLoadedNodesFromContent, { [documentInformation.metaData.documentNode]: documentInformation.metaData.documentNodeSerialization From 95bcbb923251e14999c88315a435a5dd3866b7dc Mon Sep 17 00:00:00 2001 From: Sebastian Helzle Date: Wed, 19 Jun 2024 09:35:01 +0200 Subject: [PATCH 07/13] TASK: Filter node duplicates in UI flow queries --- Classes/Controller/BackendServiceController.php | 12 +++++++----- .../NeosUiDefaultNodesOperation.php | 3 ++- .../src/initializeGuestFrame.js | 14 +++++++------- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/Classes/Controller/BackendServiceController.php b/Classes/Controller/BackendServiceController.php index 7967129d60..7224477671 100644 --- a/Classes/Controller/BackendServiceController.php +++ b/Classes/Controller/BackendServiceController.php @@ -532,11 +532,13 @@ public function flowQueryAction(array $chain): string $createContext = array_shift($chain); $finisher = array_pop($chain); + $nodeContextPaths = array_unique(array_column($createContext['payload'], '$node')); + $flowQuery = new FlowQuery(array_map( - function ($envelope) { - return $this->nodeService->getNodeFromContextPath($envelope['$node']); + function ($contextPath) { + return $this->nodeService->getNodeFromContextPath($contextPath); }, - $createContext['payload'] + $nodeContextPaths )); foreach ($chain as $operation) { @@ -548,8 +550,8 @@ function ($envelope) { switch ($finisher['type']) { case 'get': - $result = $nodeInfoHelper->renderNodes(array_filter( - $flowQuery->get()), + $result = $nodeInfoHelper->renderNodes( + array_filter($flowQuery->get()), $this->getControllerContext() ); break; diff --git a/Classes/FlowQueryOperations/NeosUiDefaultNodesOperation.php b/Classes/FlowQueryOperations/NeosUiDefaultNodesOperation.php index 141da9a32a..b198f312e4 100644 --- a/Classes/FlowQueryOperations/NeosUiDefaultNodesOperation.php +++ b/Classes/FlowQueryOperations/NeosUiDefaultNodesOperation.php @@ -72,8 +72,9 @@ public function canEvaluate($context) public function evaluate(FlowQuery $flowQuery, array $arguments) { /** @var TraversableNodeInterface $siteNode */ + $siteNode = $flowQuery->getContext()[0]; /** @var TraversableNodeInterface $documentNode */ - list($siteNode, $documentNode) = $flowQuery->getContext(); + $documentNode = $flowQuery->getContext()[1] ?? $siteNode; /** @var string[] $toggledNodes */ list($baseNodeType, $loadingDepth, $toggledNodes, $clipboardNodesContextPaths) = $arguments; diff --git a/packages/neos-ui-guest-frame/src/initializeGuestFrame.js b/packages/neos-ui-guest-frame/src/initializeGuestFrame.js index 907b0b73df..ac35822c19 100644 --- a/packages/neos-ui-guest-frame/src/initializeGuestFrame.js +++ b/packages/neos-ui-guest-frame/src/initializeGuestFrame.js @@ -64,28 +64,28 @@ export default ({globalRegistry, store}) => function * initializeGuestFrame() { // Load legacy node data scripts from guest frame - remove with Neos 9.0 const legacyNodeData = guestFrameWindow['@Neos.Neos.Ui:Nodes'] || {}; - // Load all nodedata for nodes in the guest frame + // Load all nodedata for nodes in the guest frame and filter duplicates const {q} = yield backend.get(); const nodeContextPathsInGuestFrame = findAllNodesInGuestFrame().map(node => node.getAttribute('data-__neos-node-contextpath')); - // Filter nodes that are already loaded in the redux store + // Filter nodes that are already present in the redux store and duplicates const nodesByContextPath = store.getState().cr.nodes.byContextPath; const nodesAlreadyPresentInStore = {}; - const notFullyLoadedNodeContextPaths = nodeContextPathsInGuestFrame.filter((contextPath) => { + const notFullyLoadedNodeContextPaths = [...new Set(nodeContextPathsInGuestFrame)].filter((contextPath) => { const node = nodesByContextPath[contextPath]; const nodeIsLoaded = node !== undefined && node.isFullyLoaded; - if (nodeIsLoaded){ + if (nodeIsLoaded) { nodesAlreadyPresentInStore[contextPath] = node; return false; } return true; }); - // Load remaining list of nodes from the backend - const fullyLoadedNodesFromContent = (yield q(notFullyLoadedNodeContextPaths).get()).reduce((nodes, node) => { + // Load remaining list of not fully loaded nodes from the backend if there are any + const fullyLoadedNodesFromContent = notFullyLoadedNodeContextPaths.length > 0 ? (yield q(notFullyLoadedNodeContextPaths).get()).reduce((nodes, node) => { nodes[node.contextPath] = node; return nodes; - }, {}); + }, {}) : []; const nodes = Object.assign( {}, From 93de40cee3545edf129713b8a98ee61b2f6d58c5 Mon Sep 17 00:00:00 2001 From: Sebastian Helzle Date: Thu, 27 Jun 2024 11:46:42 +0200 Subject: [PATCH 08/13] =?UTF-8?q?TASK:=20Don=E2=80=99t=20merge=20existing?= =?UTF-8?q?=20nodes=20back=20into=20the=20store?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/neos-ui-guest-frame/src/initializeGuestFrame.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/neos-ui-guest-frame/src/initializeGuestFrame.js b/packages/neos-ui-guest-frame/src/initializeGuestFrame.js index ac35822c19..548f3c53f1 100644 --- a/packages/neos-ui-guest-frame/src/initializeGuestFrame.js +++ b/packages/neos-ui-guest-frame/src/initializeGuestFrame.js @@ -87,18 +87,21 @@ export default ({globalRegistry, store}) => function * initializeGuestFrame() { return nodes; }, {}) : []; - const nodes = Object.assign( + let nodes = Object.assign( {}, legacyNodeData, // Merge legacy node data from the guest frame - remove with Neos 9.0 - nodesAlreadyPresentInStore, fullyLoadedNodesFromContent, { [documentInformation.metaData.documentNode]: documentInformation.metaData.documentNodeSerialization } ); + // Merge new nodes into the store yield put(actions.CR.Nodes.merge(nodes)); + // Combine new and existing nodes into a list for initialisation in the guest frame + nodes = Object.assign(nodes, nodesAlreadyPresentInStore); + // Remove the legacy inline scripts after initialization - remove with Neos 9.0 Array.prototype.forEach.call(guestFrameWindow.document.querySelectorAll('script[data-neos-nodedata]'), element => element.parentElement.removeChild(element)); From 9c2ae2200075451a89e4c456cf127407848f90c8 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 15 Jul 2024 16:57:16 +0200 Subject: [PATCH 09/13] TASK: Minimal cosmetically cleanup in `UserLocaleService` --- Classes/Domain/Service/UserLocaleService.php | 29 ++++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/Classes/Domain/Service/UserLocaleService.php b/Classes/Domain/Service/UserLocaleService.php index e4451b4369..eafa2d12e5 100644 --- a/Classes/Domain/Service/UserLocaleService.php +++ b/Classes/Domain/Service/UserLocaleService.php @@ -15,6 +15,7 @@ use Neos\Flow\I18n\Locale; use Neos\Flow\I18n\Service as I18nService; use Neos\Neos\Domain\Service\UserService; +use Neos\Neos\Fusion\Helper\NodeLabelToken; class UserLocaleService { @@ -38,14 +39,17 @@ class UserLocaleService protected $rememberedContentLocale; /** - * Remebered content locale for locale switching + * The current user's locale (cached for performance) * * @var Locale */ - protected $rememberedUserLocale; + protected $userLocaleRuntimeCache; /** * For serialization, we need to respect the UI locale, rather than the content locale + * This is done to translate the node labels correctly. + * For example {@see NodeLabelToken::resolveLabelFromNodeType()} will call the translator which will uses the globally set locale. + * FIXME we should eliminate hacking the global state and passing the locale differently * * @param boolean $reset Reset to remebered locale */ @@ -54,16 +58,17 @@ public function switchToUILocale($reset = false) if ($reset === true) { // Reset the locale $this->i18nService->getConfiguration()->setCurrentLocale($this->rememberedContentLocale); - } elseif ($this->rememberedUserLocale) { - // Restore the local - $this->i18nService->getConfiguration()->setCurrentLocale($this->rememberedUserLocale); - } else { - $this->rememberedContentLocale = $this->i18nService->getConfiguration()->getCurrentLocale(); - $userLocalePreference = ($this->userService->getCurrentUser() ? $this->userService->getCurrentUser()->getPreferences()->getInterfaceLanguage() : null); - $defaultLocale = $this->i18nService->getConfiguration()->getDefaultLocale(); - $userLocale = $userLocalePreference ? new Locale($userLocalePreference) : $defaultLocale; - $this->rememberedUserLocale = $userLocale; - $this->i18nService->getConfiguration()->setCurrentLocale($userLocale); + return; + } + $this->rememberedContentLocale = $this->i18nService->getConfiguration()->getCurrentLocale(); + if ($this->userLocaleRuntimeCache) { + $this->i18nService->getConfiguration()->setCurrentLocale($this->userLocaleRuntimeCache); + return; } + $userLocalePreference = ($this->userService->getCurrentUser() ? $this->userService->getCurrentUser()->getPreferences()->getInterfaceLanguage() : null); + $defaultLocale = $this->i18nService->getConfiguration()->getDefaultLocale(); + $userLocale = $userLocalePreference ? new Locale($userLocalePreference) : $defaultLocale; + $this->userLocaleRuntimeCache = $userLocale; + $this->i18nService->getConfiguration()->setCurrentLocale($userLocale); } } From b82dc845fabfbcb9aeb874cacf97f2bb7c4e074e Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 15 Jul 2024 17:30:54 +0200 Subject: [PATCH 10/13] TASK: Dont pass `nodes` explicitly to `initializeContentDomNode` but use the store previously it was cumbersome to collect all the `nodes` that are really on the page. But it should make no difference to do the lookup on a bigger set. --- .../src/initializeContentDomNode.js | 3 ++- .../src/initializeGuestFrame.js | 17 ++++------------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/packages/neos-ui-guest-frame/src/initializeContentDomNode.js b/packages/neos-ui-guest-frame/src/initializeContentDomNode.js index 069086fdd0..d3577f8ca6 100644 --- a/packages/neos-ui-guest-frame/src/initializeContentDomNode.js +++ b/packages/neos-ui-guest-frame/src/initializeContentDomNode.js @@ -10,7 +10,8 @@ import initializePropertyDomNode from './initializePropertyDomNode'; import style from './style.module.css'; -export default ({store, globalRegistry, nodeTypesRegistry, inlineEditorRegistry, nodes}) => contentDomNode => { +export default ({store, globalRegistry, nodeTypesRegistry, inlineEditorRegistry}) => contentDomNode => { + const nodes = store.getState().cr.nodes.byContextPath; const contextPath = contentDomNode.getAttribute('data-__neos-node-contextpath'); if (!nodes[contextPath]) { diff --git a/packages/neos-ui-guest-frame/src/initializeGuestFrame.js b/packages/neos-ui-guest-frame/src/initializeGuestFrame.js index 548f3c53f1..631e1a737b 100644 --- a/packages/neos-ui-guest-frame/src/initializeGuestFrame.js +++ b/packages/neos-ui-guest-frame/src/initializeGuestFrame.js @@ -70,24 +70,19 @@ export default ({globalRegistry, store}) => function * initializeGuestFrame() { // Filter nodes that are already present in the redux store and duplicates const nodesByContextPath = store.getState().cr.nodes.byContextPath; - const nodesAlreadyPresentInStore = {}; const notFullyLoadedNodeContextPaths = [...new Set(nodeContextPathsInGuestFrame)].filter((contextPath) => { const node = nodesByContextPath[contextPath]; const nodeIsLoaded = node !== undefined && node.isFullyLoaded; - if (nodeIsLoaded) { - nodesAlreadyPresentInStore[contextPath] = node; - return false; - } - return true; + return !nodeIsLoaded; }); // Load remaining list of not fully loaded nodes from the backend if there are any const fullyLoadedNodesFromContent = notFullyLoadedNodeContextPaths.length > 0 ? (yield q(notFullyLoadedNodeContextPaths).get()).reduce((nodes, node) => { nodes[node.contextPath] = node; return nodes; - }, {}) : []; + }, {}) : {}; - let nodes = Object.assign( + const nodes = Object.assign( {}, legacyNodeData, // Merge legacy node data from the guest frame - remove with Neos 9.0 fullyLoadedNodesFromContent, @@ -99,9 +94,6 @@ export default ({globalRegistry, store}) => function * initializeGuestFrame() { // Merge new nodes into the store yield put(actions.CR.Nodes.merge(nodes)); - // Combine new and existing nodes into a list for initialisation in the guest frame - nodes = Object.assign(nodes, nodesAlreadyPresentInStore); - // Remove the legacy inline scripts after initialization - remove with Neos 9.0 Array.prototype.forEach.call(guestFrameWindow.document.querySelectorAll('script[data-neos-nodedata]'), element => element.parentElement.removeChild(element)); @@ -186,8 +178,7 @@ export default ({globalRegistry, store}) => function * initializeGuestFrame() { store, globalRegistry, nodeTypesRegistry, - inlineEditorRegistry, - nodes + inlineEditorRegistry }); requestIdleCallback(() => { From 7314497764960c5b3175f4526471ae443432bf98 Mon Sep 17 00:00:00 2001 From: mhsdesign <85400359+mhsdesign@users.noreply.github.com> Date: Mon, 15 Jul 2024 17:38:06 +0200 Subject: [PATCH 11/13] TASK: Add comment that we deduplicate the passed nodes to the flowquery endpoint that means `neosUiDefaultNodes` only needs one param. Technically we use a `Set` most times when we invoke the `q` endpoint but seb convinced me that this filtering doesnt hurt and is probably more sane :D --- Classes/Controller/BackendServiceController.php | 1 + packages/neos-ui-sagas/src/UI/ContentTree/index.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Classes/Controller/BackendServiceController.php b/Classes/Controller/BackendServiceController.php index 7224477671..722eb66b7e 100644 --- a/Classes/Controller/BackendServiceController.php +++ b/Classes/Controller/BackendServiceController.php @@ -532,6 +532,7 @@ public function flowQueryAction(array $chain): string $createContext = array_shift($chain); $finisher = array_pop($chain); + // we deduplicate passed nodes here $nodeContextPaths = array_unique(array_column($createContext['payload'], '$node')); $flowQuery = new FlowQuery(array_map( diff --git a/packages/neos-ui-sagas/src/UI/ContentTree/index.js b/packages/neos-ui-sagas/src/UI/ContentTree/index.js index a68d024ac1..cc59c24d5d 100644 --- a/packages/neos-ui-sagas/src/UI/ContentTree/index.js +++ b/packages/neos-ui-sagas/src/UI/ContentTree/index.js @@ -152,7 +152,7 @@ export function * watchCurrentDocument({globalRegistry, configuration}) { yield put(actions.UI.ContentTree.setAsLoading(contextPath)); const nodeTypeFilter = `${nodeTypesRegistry.getRole('contentCollection')},${nodeTypesRegistry.getRole('content')}`; - const nodes = yield q([contextPath, contextPath]).neosUiDefaultNodes( + const nodes = yield q([contextPath]).neosUiDefaultNodes( nodeTypeFilter, configuration.structureTree.loadingDepth, [], From 0d39c2e599f1b13a047a83f31bbb63034daf1c71 Mon Sep 17 00:00:00 2001 From: Sebastian Helzle Date: Tue, 23 Jul 2024 16:21:00 +0200 Subject: [PATCH 12/13] TASK: Fix styleci issue --- Classes/Controller/BackendServiceController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Classes/Controller/BackendServiceController.php b/Classes/Controller/BackendServiceController.php index 722eb66b7e..54dfa87b20 100644 --- a/Classes/Controller/BackendServiceController.php +++ b/Classes/Controller/BackendServiceController.php @@ -567,7 +567,7 @@ function ($contextPath) { $nodeTypeFilter = $finisher['payload']['nodeTypeFilter'] ?? null; $result = $nodeInfoHelper->renderNodesWithParents( array_filter($flowQuery->get()), - $this->getControllerContext(), + $this->getControllerContext(), $nodeTypeFilter ); break; From f4520af8a01e3eaa2ec2a9bd1b69a18096e4f873 Mon Sep 17 00:00:00 2001 From: Sebastian Helzle Date: Thu, 8 Aug 2024 09:34:11 +0200 Subject: [PATCH 13/13] BUGFIX: Make sure to have the latest content when switching pages This prevents showing outdated content, as the guest frame will not load new content by itself anymore. --- .../neos-ui-sagas/src/UI/ContentTree/index.js | 41 ++++++++----------- 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/packages/neos-ui-sagas/src/UI/ContentTree/index.js b/packages/neos-ui-sagas/src/UI/ContentTree/index.js index cc59c24d5d..ac9b513971 100644 --- a/packages/neos-ui-sagas/src/UI/ContentTree/index.js +++ b/packages/neos-ui-sagas/src/UI/ContentTree/index.js @@ -141,30 +141,23 @@ export function * watchCurrentDocument({globalRegistry, configuration}) { return; } - const state = yield select(); - const childrenAreFullyLoaded = $get(['cr', 'nodes', 'byContextPath', contextPath, 'children'], state) - .filter(childEnvelope => nodeTypesRegistry.hasRole(childEnvelope.nodeType, 'content') || nodeTypesRegistry.hasRole(childEnvelope.nodeType, 'contentCollection')) - .every( - childEnvelope => Boolean($get(['cr', 'nodes', 'byContextPath', $get('contextPath', childEnvelope)], state)) - ); - - if (!childrenAreFullyLoaded) { - yield put(actions.UI.ContentTree.setAsLoading(contextPath)); - - const nodeTypeFilter = `${nodeTypesRegistry.getRole('contentCollection')},${nodeTypesRegistry.getRole('content')}`; - const nodes = yield q([contextPath]).neosUiDefaultNodes( - nodeTypeFilter, - configuration.structureTree.loadingDepth, - [], - [] - ).get(); - const nodeMap = nodes.reduce((nodeMap, node) => { - nodeMap[$get('contextPath', node)] = node; - return nodeMap; - }, {}); + // Always reload the nodes for the current page, when the document node changes. + // In the past, the guest frame loaded the latest nodedata from the rendered content, but this has been removed. + yield put(actions.UI.ContentTree.setAsLoading(contextPath)); - yield put(actions.CR.Nodes.merge(nodeMap)); - yield put(actions.UI.ContentTree.setAsLoaded(contextPath)); - } + const nodeTypeFilter = `${nodeTypesRegistry.getRole('contentCollection')},${nodeTypesRegistry.getRole('content')}`; + const nodes = yield q([contextPath]).neosUiDefaultNodes( + nodeTypeFilter, + configuration.structureTree.loadingDepth, + [], + [] + ).get(); + const nodeMap = nodes.reduce((nodeMap, node) => { + nodeMap[$get('contextPath', node)] = node; + return nodeMap; + }, {}); + + yield put(actions.CR.Nodes.merge(nodeMap)); + yield put(actions.UI.ContentTree.setAsLoaded(contextPath)); }); }