diff --git a/Classes/Controller/BackendController.php b/Classes/Controller/BackendController.php index 643307acf9..6d0d03a6b6 100644 --- a/Classes/Controller/BackendController.php +++ b/Classes/Controller/BackendController.php @@ -12,36 +12,37 @@ * source code. */ -use Neos\ContentRepository\Core\NodeType\NodeTypeName; use Neos\ContentRepository\Core\Projection\ContentGraph\VisibilityConstraints; use Neos\ContentRepositoryRegistry\ContentRepositoryRegistry; use Neos\Flow\Annotations as Flow; use Neos\Flow\Mvc\Controller\ActionController; -use Neos\Flow\Mvc\View\ViewInterface; use Neos\Flow\Persistence\PersistenceManagerInterface; -use Neos\Flow\ResourceManagement\ResourceManager; use Neos\Flow\Security\Context; use Neos\Flow\Session\SessionInterface; -use Neos\Fusion\View\FusionView; -use Neos\Neos\Controller\Backend\MenuHelper; use Neos\Neos\Domain\Repository\DomainRepository; use Neos\Neos\Domain\Repository\SiteRepository; use Neos\Neos\Domain\Service\NodeTypeNameFactory; use Neos\Neos\Domain\Service\WorkspaceNameBuilder; use Neos\Neos\FrontendRouting\NodeAddressFactory; use Neos\Neos\FrontendRouting\SiteDetection\SiteDetectionResult; -use Neos\Neos\Service\BackendRedirectionService; use Neos\Neos\Service\UserService; -use Neos\Neos\Ui\Domain\Service\StyleAndJavascriptInclusionService; -use Neos\Neos\Ui\Service\NodeClipboard; +use Neos\Neos\Ui\Domain\InitialData\ConfigurationProviderInterface; +use Neos\Neos\Ui\Domain\InitialData\FrontendConfigurationProviderInterface; +use Neos\Neos\Ui\Domain\InitialData\InitialStateProviderInterface; +use Neos\Neos\Ui\Domain\InitialData\MenuProviderInterface; +use Neos\Neos\Ui\Domain\InitialData\NodeTypeGroupsAndRolesProviderInterface; +use Neos\Neos\Ui\Domain\InitialData\RoutesProviderInterface; +use Neos\Neos\Ui\Presentation\ApplicationView; class BackendController extends ActionController { /** - * @var FusionView + * @var ApplicationView */ protected $view; + protected $defaultViewObjectName = ApplicationView::class; + /** * @Flow\Inject * @var UserService @@ -74,57 +75,51 @@ class BackendController extends ActionController /** * @Flow\Inject - * @var ResourceManager + * @var ContentRepositoryRegistry */ - protected $resourceManager; + protected $contentRepositoryRegistry; /** * @Flow\Inject - * @var MenuHelper + * @var Context */ - protected $menuHelper; + protected $securityContext; /** - * @Flow\Inject(lazy=false) - * @var BackendRedirectionService + * @Flow\Inject + * @var ConfigurationProviderInterface */ - protected $backendRedirectionService; + protected $configurationProvider; /** * @Flow\Inject - * @var ContentRepositoryRegistry + * @var RoutesProviderInterface */ - protected $contentRepositoryRegistry; + protected $routesProvider; /** * @Flow\Inject - * @var Context + * @var FrontendConfigurationProviderInterface */ - protected $securityContext; + protected $frontendConfigurationProvider; /** * @Flow\Inject - * @var StyleAndJavascriptInclusionService + * @var NodeTypeGroupsAndRolesProviderInterface */ - protected $styleAndJavascriptInclusionService; + protected $nodeTypeGroupsAndRolesProvider; /** * @Flow\Inject - * @var NodeClipboard + * @var MenuProviderInterface */ - protected $clipboard; + protected $menuProvider; /** - * @Flow\InjectConfiguration(package="Neos.Neos.Ui", path="splashScreen.partial") - * @var string + * @Flow\Inject + * @var InitialStateProviderInterface */ - protected $splashScreenPartial; - - public function initializeView(ViewInterface $view) - { - /** @var FusionView $view */ - $view->setFusionPath('backend'); - } + protected $initialStateProvider; /** * Displays the backend interface @@ -187,23 +182,36 @@ public function indexAction(string $node = null) $node = $subgraph->findNodeById($nodeAddress->nodeAggregateId); } - $this->view->assign('user', $user); - $this->view->assign('documentNode', $node); - $this->view->assign('site', $siteNode); - $this->view->assign('clipboardNodes', $this->clipboard->getSerializedNodeAddresses()); - $this->view->assign('clipboardMode', $this->clipboard->getMode()); - $this->view->assign('headScripts', $this->styleAndJavascriptInclusionService->getHeadScripts()); - $this->view->assign('headStylesheets', $this->styleAndJavascriptInclusionService->getHeadStylesheets()); - $this->view->assign('splashScreenPartial', $this->splashScreenPartial); - $this->view->assign('sitesForMenu', $this->menuHelper->buildSiteList($this->getControllerContext())); - $this->view->assign('modulesForMenu', $this->menuHelper->buildModuleList($this->getControllerContext())); - $this->view->assign('contentRepositoryId', $siteDetectionResult->contentRepositoryId); - - $this->view->assignMultiple([ - 'subgraph' => $subgraph + $this->view->setOption('title', 'Neos CMS'); + $this->view->assign('initialData', [ + 'configuration' => + $this->configurationProvider->getConfiguration( + contentRepository: $contentRepository, + uriBuilder: $this->controllerContext->getUriBuilder(), + ), + 'routes' => + $this->routesProvider->getRoutes( + uriBuilder: $this->controllerContext->getUriBuilder() + ), + 'frontendConfiguration' => + $this->frontendConfigurationProvider->getFrontendConfiguration( + controllerContext: $this->controllerContext, + ), + 'nodeTypes' => + $this->nodeTypeGroupsAndRolesProvider->getNodeTypes(), + 'menu' => + $this->menuProvider->getMenu( + controllerContext: $this->controllerContext, + ), + 'initialState' => + $this->initialStateProvider->getInitialState( + controllerContext: $this->controllerContext, + contentRepositoryId: $siteDetectionResult->contentRepositoryId, + documentNode: $node, + site: $siteNode, + user: $user, + ), ]); - - $this->view->assign('interfaceLanguage', $this->userService->getInterfaceLanguage()); } /** diff --git a/Classes/Domain/InitialData/CacheConfigurationVersionProviderInterface.php b/Classes/Domain/InitialData/CacheConfigurationVersionProviderInterface.php new file mode 100644 index 0000000000..13982abb82 --- /dev/null +++ b/Classes/Domain/InitialData/CacheConfigurationVersionProviderInterface.php @@ -0,0 +1,27 @@ + */ + public function getFrontendConfiguration( + ControllerContext $controllerContext + ): array; +} diff --git a/Classes/Domain/InitialData/InitialStateProviderInterface.php b/Classes/Domain/InitialData/InitialStateProviderInterface.php new file mode 100644 index 0000000000..37eeba456c --- /dev/null +++ b/Classes/Domain/InitialData/InitialStateProviderInterface.php @@ -0,0 +1,38 @@ + */ + public function getInitialState( + ControllerContext $controllerContext, + ContentRepositoryId $contentRepositoryId, + ?Node $documentNode, + ?Node $site, + User $user, + ): array; +} diff --git a/Classes/Domain/InitialData/MenuProviderInterface.php b/Classes/Domain/InitialData/MenuProviderInterface.php new file mode 100644 index 0000000000..d91f4434f8 --- /dev/null +++ b/Classes/Domain/InitialData/MenuProviderInterface.php @@ -0,0 +1,31 @@ +}> + */ + public function getMenu(ControllerContext $controllerContext): array; +} diff --git a/Classes/Domain/InitialData/NodeTypeGroupsAndRolesProviderInterface.php b/Classes/Domain/InitialData/NodeTypeGroupsAndRolesProviderInterface.php new file mode 100644 index 0000000000..65eb9de3ab --- /dev/null +++ b/Classes/Domain/InitialData/NodeTypeGroupsAndRolesProviderInterface.php @@ -0,0 +1,32 @@ + */ + public function getRoutes(UriBuilder $uriBuilder): array; +} diff --git a/Classes/Domain/Service/StyleAndJavascriptInclusionService.php b/Classes/Domain/Service/StyleAndJavascriptInclusionService.php index 429acb92aa..c54465ed43 100644 --- a/Classes/Domain/Service/StyleAndJavascriptInclusionService.php +++ b/Classes/Domain/Service/StyleAndJavascriptInclusionService.php @@ -61,7 +61,7 @@ class StyleAndJavascriptInclusionService public function getHeadScripts(): string { return $this->build($this->javascriptResources, function ($uri, $additionalAttributes) { - return ''; + return ''; }); } @@ -99,11 +99,14 @@ protected function build(array $resourceArrayToSort, \Closure $builderForLine): $resourceExpression = $this->resourceManager->getPublicPackageResourceUriByPath($resourceExpression); } $finalUri = $hash ? $resourceExpression . '?' . $hash : $resourceExpression; - $additionalAttributes = array_merge( - // legacy first level 'defer' attribute - isset($element['defer']) ? ['defer' => $element['defer']] : [], - $element['attributes'] ?? [] - ); + $additionalAttributes = $element['attributes'] ?? []; + + // All scripts are deferred by default. This prevents the attribute from + // being specified redundantly. + if (isset($additionalAttributes['defer'])) { + unset($additionalAttributes['defer']); + } + $result .= $builderForLine($finalUri, $this->htmlAttributesArrayToString($additionalAttributes)); } return $result; diff --git a/Classes/Infrastructure/Cache/CacheConfigurationVersionProvider.php b/Classes/Infrastructure/Cache/CacheConfigurationVersionProvider.php new file mode 100644 index 0000000000..e9358122da --- /dev/null +++ b/Classes/Infrastructure/Cache/CacheConfigurationVersionProvider.php @@ -0,0 +1,69 @@ +computedCacheConfigurationVersion ??= + $this->computeCacheConfigurationVersion(); + } + + private function computeCacheConfigurationVersion(): string + { + /** @var ?Account $account */ + $account = $this->securityContext->getAccount(); + + // Get all roles and sort them by identifier + $roles = $account ? array_map(static fn ($role) => $role->getIdentifier(), $account->getRoles()) : []; + sort($roles); + + // Use the roles combination as cache key to allow multiple users sharing the same configuration version + $configurationIdentifier = md5(implode('_', $roles)); + $cacheKey = 'ConfigurationVersion_' . $configurationIdentifier; + /** @var string|false $version */ + $version = $this->configurationCache->get($cacheKey); + + if ($version === false) { + $version = (string)time(); + $this->configurationCache->set($cacheKey, $version); + } + return $configurationIdentifier . '_' . $version; + } +} diff --git a/Classes/Infrastructure/Configuration/ConfigurationProvider.php b/Classes/Infrastructure/Configuration/ConfigurationProvider.php new file mode 100644 index 0000000000..36e4410d53 --- /dev/null +++ b/Classes/Infrastructure/Configuration/ConfigurationProvider.php @@ -0,0 +1,92 @@ + $this->configurationManager->getConfiguration( + ConfigurationManager::CONFIGURATION_TYPE_SETTINGS, + 'Neos.Neos.userInterface.navigateComponent.nodeTree', + ), + 'structureTree' => $this->configurationManager->getConfiguration( + ConfigurationManager::CONFIGURATION_TYPE_SETTINGS, + 'Neos.Neos.userInterface.navigateComponent.structureTree', + ), + 'allowedTargetWorkspaces' => + $this->workspaceService->getAllowedTargetWorkspaces( + $contentRepository + ), + 'endpoints' => [ + 'nodeTypeSchema' => $uriBuilder->reset() + ->setCreateAbsoluteUri(true) + ->uriFor( + actionName: 'nodeTypeSchema', + controllerArguments: [ + 'version' => + $this->cacheConfigurationVersionProvider + ->getCacheConfigurationVersion(), + ], + controllerName: 'Backend\\Schema', + packageKey: 'Neos.Neos', + ), + 'translations' => $uriBuilder->reset() + ->setCreateAbsoluteUri(true) + ->uriFor( + actionName: 'xliffAsJson', + controllerArguments: [ + 'locale' => + $this->userService + ->getInterfaceLanguage(), + 'version' => + $this->cacheConfigurationVersionProvider + ->getCacheConfigurationVersion(), + ], + controllerName: 'Backend\\Backend', + packageKey: 'Neos.Neos', + ), + ] + ]; + } +} diff --git a/Classes/Infrastructure/Configuration/FrontendConfigurationProvider.php b/Classes/Infrastructure/Configuration/FrontendConfigurationProvider.php new file mode 100644 index 0000000000..7d5565fd37 --- /dev/null +++ b/Classes/Infrastructure/Configuration/FrontendConfigurationProvider.php @@ -0,0 +1,43 @@ + */ + #[Flow\InjectConfiguration('frontendConfiguration')] + protected array $frontendConfigurationBeforeProcessing; + + public function getFrontendConfiguration( + ControllerContext $controllerContext + ): array { + return $this->configurationRenderingService->computeConfiguration( + $this->frontendConfigurationBeforeProcessing, + ['controllerContext' => $controllerContext] + ); + } +} diff --git a/Classes/Infrastructure/Configuration/InitialStateProvider.php b/Classes/Infrastructure/Configuration/InitialStateProvider.php new file mode 100644 index 0000000000..ff984c40fb --- /dev/null +++ b/Classes/Infrastructure/Configuration/InitialStateProvider.php @@ -0,0 +1,59 @@ + */ + #[Flow\InjectConfiguration('initialState')] + protected array $initialStateBeforeProcessing; + + public function getInitialState( + ControllerContext $controllerContext, + ContentRepositoryId $contentRepositoryId, + ?Node $documentNode, + ?Node $site, + User $user, + ): array { + return $this->configurationRenderingService->computeConfiguration( + $this->initialStateBeforeProcessing, + [ + 'controllerContext' => $controllerContext, + 'contentRepositoryId' => $contentRepositoryId, + 'documentNode' => $documentNode, + 'site' => $site, + 'user' => $user, + 'clipboardNodes' => $this->clipboard->getSerializedNodeAddresses(), + 'clipboardMode' => $this->clipboard->getMode(), + ] + ); + } +} diff --git a/Classes/Infrastructure/ContentRepository/NodeTypeGroupsAndRolesProvider.php b/Classes/Infrastructure/ContentRepository/NodeTypeGroupsAndRolesProvider.php new file mode 100644 index 0000000000..429702eaa2 --- /dev/null +++ b/Classes/Infrastructure/ContentRepository/NodeTypeGroupsAndRolesProvider.php @@ -0,0 +1,38 @@ + */ + #[Flow\InjectConfiguration(path: 'nodeTypeRoles')] + protected array $roles; + + /** @var array */ + #[Flow\InjectConfiguration(path: 'nodeTypes.groups', package: 'Neos.Neos')] + protected array $groups; + + public function getNodeTypes(): array + { + return [ + 'roles' => $this->roles, + 'groups' => $this->groups, + ]; + } +} diff --git a/Classes/Infrastructure/MVC/RoutesProvider.php b/Classes/Infrastructure/MVC/RoutesProvider.php new file mode 100644 index 0000000000..f27d03af3a --- /dev/null +++ b/Classes/Infrastructure/MVC/RoutesProvider.php @@ -0,0 +1,178 @@ + + $helper->buildUiServiceRoute('change'), + 'publish' => + $helper->buildUiServiceRoute('publish'), + 'discard' => + $helper->buildUiServiceRoute('discard'), + 'changeBaseWorkspace' => + $helper->buildUiServiceRoute('changeBaseWorkspace'), + 'rebaseWorkspace' => + $helper->buildUiServiceRoute('rebaseWorkspace'), + 'copyNodes' => + $helper->buildUiServiceRoute('copyNodes'), + 'cutNodes' => + $helper->buildUiServiceRoute('cutNodes'), + 'clearClipboard' => + $helper->buildUiServiceRoute('clearClipboard'), + 'flowQuery' => + $helper->buildUiServiceRoute('flowQuery'), + 'generateUriPathSegment' => + $helper->buildUiServiceRoute('generateUriPathSegment'), + 'getWorkspaceInfo' => + $helper->buildUiServiceRoute('getWorkspaceInfo'), + 'getAdditionalNodeMetadata' => + $helper->buildUiServiceRoute('getAdditionalNodeMetadata'), + ]; + + $routes['core']['content'] = [ + 'imageWithMetadata' => + $helper->buildCoreRoute( + controllerName: 'Backend\\Content', + actionName: 'imageWithMetaData' + ), + 'createImageVariant' => + $helper->buildCoreRoute( + controllerName: 'Backend\\Content', + actionName: 'createImageVariant' + ), + 'loadMasterPlugins' => + $helper->buildCoreRoute( + controllerName: 'Backend\\Content', + actionName: 'masterPlugins' + ), + 'loadPluginViews' => + $helper->buildCoreRoute( + controllerName: 'Backend\\Content', + actionName: 'pluginViews' + ), + 'uploadAsset' => + $helper->buildCoreRoute( + controllerName: 'Backend\\Content', + actionName: 'uploadAsset' + ), + ]; + + $routes['core']['service'] = [ + 'assetProxies' => + $helper->buildCoreRoute( + controllerName: 'Service\\AssetProxies', + actionName: 'index' + ), + 'assets' => + $helper->buildCoreRoute( + controllerName: 'Service\\Assets', + actionName: 'index' + ), + 'nodes' => + $helper->buildCoreRoute( + controllerName: 'Service\\Nodes', + actionName: 'index' + ), + 'userPreferences' => + $helper->buildCoreRoute( + subPackageKey: 'Service', + controllerName: 'UserPreference', + actionName: 'index', + format: 'json', + ), + 'dataSource' => + $helper->buildCoreRoute( + subPackageKey: 'Service', + controllerName: 'DataSource', + actionName: 'index', + format: 'json', + ), + 'contentDimensions' => + $helper->buildCoreRoute( + controllerName: 'Service\\ContentDimensions', + actionName: 'index', + ), + 'impersonateStatus' => + $helper->buildCoreRoute( + controllerName: 'Backend\\Impersonate', + actionName: 'status', + format: 'json', + ), + 'impersonateRestore' => + $helper->buildCoreRoute( + controllerName: 'Backend\\Impersonate', + actionName: 'restoreWithResponse', + format: 'json', + ), + ]; + + $routes['core']['modules'] = [ + 'workspaces' => + $helper->buildCoreRoute( + controllerName: 'Backend\\Module', + actionName: 'index', + arguments: [ + 'module' => 'management/workspaces' + ] + ), + 'userSettings' => + $helper->buildCoreRoute( + controllerName: 'Backend\\Module', + actionName: 'index', + arguments: [ + 'module' => 'user/usersettings' + ] + ), + 'mediaBrowser' => + $helper->buildCoreRoute( + controllerName: 'Backend\\Module', + actionName: 'index', + arguments: [ + 'module' => 'media/browser' + ] + ), + ]; + + $routes['core']['login'] = + $helper->buildCoreRoute( + controllerName: 'Login', + actionName: 'index', + format: 'json', + ); + + $routes['core']['logout'] = + $helper->buildCoreRoute( + controllerName: 'Login', + actionName: 'logout', + ); + + return $routes; + } +} diff --git a/Classes/Infrastructure/MVC/RoutesProviderHelper.php b/Classes/Infrastructure/MVC/RoutesProviderHelper.php new file mode 100644 index 0000000000..5ac1acca87 --- /dev/null +++ b/Classes/Infrastructure/MVC/RoutesProviderHelper.php @@ -0,0 +1,68 @@ +uriBuilder->reset() + ->setCreateAbsoluteUri(true) + ->uriFor( + actionName: $actionName, + controllerArguments: [], + controllerName: 'BackendService', + packageKey: 'Neos.Neos.Ui', + ); + } + + /** + * @param array $arguments + */ + public function buildCoreRoute( + string $controllerName, + string $actionName, + ?string $subPackageKey = null, + ?string $format = null, + array $arguments = [], + ): string { + $this->uriBuilder->reset() + ->setCreateAbsoluteUri(true); + + if ($format !== null) { + $this->uriBuilder->setFormat($format); + } + + return $this->uriBuilder->uriFor( + actionName: $actionName, + controllerArguments: $arguments, + controllerName: $controllerName, + packageKey: 'Neos.Neos', + subPackageKey: $subPackageKey, + ); + } +} diff --git a/Classes/Infrastructure/Neos/MenuProvider.php b/Classes/Infrastructure/Neos/MenuProvider.php new file mode 100644 index 0000000000..43d146c414 --- /dev/null +++ b/Classes/Infrastructure/Neos/MenuProvider.php @@ -0,0 +1,110 @@ +menuHelper->buildModuleList($controllerContext); + + $result = []; + foreach ($modulesForMenu as $moduleName => $module) { + if ($module['hideInMenu'] === true) { + continue; + } + + $result[$moduleName]['label'] = $module['label']; + $result[$moduleName]['icon'] = $module['icon']; + $result[$moduleName]['uri'] = $module['uri']; + $result[$moduleName]['target'] = 'Window'; + + $result[$moduleName]['children'] = match ($module['module']) { + 'content' => $this->buildChildrenForSites($controllerContext), + default => $this->buildChildrenForBackendModule($module), + }; + } + + return array_values($result); + } + + /** + * @return array + */ + private function buildChildrenForSites(ControllerContext $controllerContext): array + { + $sitesForMenu = $this->menuHelper->buildSiteList($controllerContext); + + $result = []; + foreach ($sitesForMenu as $index => $site) { + $name = $site['name']; + $name = is_string($name) ? $name : 'N/A'; + + $uri = $site['uri']; + $uri = is_string($uri) ? $uri : '#'; + + $active = $site['active']; + $active = is_bool($active) || is_numeric($active) + ? (bool) $active + : false; + + $result[$index]['icon'] = 'globe'; + $result[$index]['label'] = $name; + $result[$index]['uri'] = $uri; + $result[$index]['target'] = 'Window'; + $result[$index]['isActive'] = $active; + $result[$index]['skipI18n'] = true; + } + + return array_values($result); + } + + /** + * @param array{submodules:array} $module + * @return array + */ + private function buildChildrenForBackendModule(array $module): array + { + $result = []; + foreach ($module['submodules'] as $submoduleName => $submodule) { + if ($submodule['hideInMenu'] === true) { + continue; + } + + $result[$submoduleName]['icon'] = $submodule['icon']; + $result[$submoduleName]['label'] = $submodule['label']; + $result[$submoduleName]['uri'] = $submodule['uri']; + $result[$submoduleName]['position'] = $submodule['position']; + $result[$submoduleName]['isActive'] = true; + $result[$submoduleName]['target'] = 'Window'; + $result[$submoduleName]['skipI18n'] = false; + } + + $positionalArraySorter = new PositionalArraySorter($result); + $result = $positionalArraySorter->toArray(); + + return array_values($result); + } +} diff --git a/Classes/Presentation/ApplicationView.php b/Classes/Presentation/ApplicationView.php new file mode 100644 index 0000000000..45ee36cf23 --- /dev/null +++ b/Classes/Presentation/ApplicationView.php @@ -0,0 +1,171 @@ + + */ + protected $supportedOptions = [ + 'title' => [null, 'The application title which will be used as the HTML .', 'string'], + ]; + + public function render(): string + { + $result = '<!DOCTYPE html>'; + $result .= '<html lang="' . $this->renderLang() . '">'; + $result .= '<head>'; + $result .= $this->renderHead(); + $result .= '</head>'; + $result .= '<body>'; + $result .= $this->renderBody(); + $result .= '</body>'; + $result .= '</html>'; + + return $result; + } + + private function renderLang(): string + { + return $this->userService->getInterfaceLanguage(); + } + + private function renderHead(): string + { + $result = '<meta charset="UTF-8">'; + $result .= '<meta name="viewport" content="width=device-width, initial-scale=1.0">'; + + $result .= '<title>' . $this->options['title'] . ''; + + $result .= $this->styleAndJavascriptInclusionService->getHeadStylesheets(); + $result .= $this->styleAndJavascriptInclusionService->getHeadScripts(); + + $result .= sprintf( + '', + $this->resourceManager->getPublicPackageResourceUriByPath( + 'resource://Neos.Neos.Ui/Public/Images/apple-touch-icon.png' + ) + ); + $result .= sprintf( + '', + $this->resourceManager->getPublicPackageResourceUriByPath( + 'resource://Neos.Neos.Ui/Public/Images/favicon-16x16.png' + ) + ); + $result .= sprintf( + '', + $this->resourceManager->getPublicPackageResourceUriByPath( + 'resource://Neos.Neos.Ui/Public/Images/favicon-32x32.png' + ) + ); + $result .= sprintf( + '', + $this->resourceManager->getPublicPackageResourceUriByPath( + 'resource://Neos.Neos.Ui/Public/Images/safari-pinned-tab.svg' + ) + ); + + $result .= sprintf( + '', + json_encode($this->variables['initialData']), + ); + + return $result; + } + + private function renderBody(): string + { + $result = sprintf( + '
', + $this->securityContext->getCsrfProtectionToken(), + (string) $this->bootstrap->getContext(), + ); + $result .= $this->renderSplashScreen(); + $result .= '
'; + + return $result; + } + + private function renderSplashScreen(): string + { + return << + @keyframes color_change { + 0% { + filter: drop-shadow(0 0 0 #00adee) opacity(25%); + } + 100% { + filter: drop-shadow(0 0 5px #00adee) opacity(100%); + } + } + + .loadingIcon { + color: #00adee; + animation-name: color_change; + animation-duration: 1.2s; + animation-iteration-count: infinite; + animation-direction: alternate; + animation-timing-function: ease-in-out; + } + .splash { + width: 100vw; + height: 100vh; + background-color: #222222; + display: flex; + align-items: center; + justify-content: center; + font-size: 30px; + } + +
+ +
+ HTML; + } +} diff --git a/Configuration/Objects.yaml b/Configuration/Objects.yaml new file mode 100644 index 0000000000..9c055c11f9 --- /dev/null +++ b/Configuration/Objects.yaml @@ -0,0 +1,33 @@ +# +# InitialData Providers for booting the UI +# +Neos\Neos\Ui\Domain\InitialData\CacheConfigurationVersionProviderInterface: + className: Neos\Neos\Ui\Infrastructure\Cache\CacheConfigurationVersionProvider + +Neos\Neos\Ui\Domain\InitialData\ConfigurationProviderInterface: + className: Neos\Neos\Ui\Infrastructure\Configuration\ConfigurationProvider + +Neos\Neos\Ui\Domain\InitialData\FrontendConfigurationProviderInterface: + className: Neos\Neos\Ui\Infrastructure\Configuration\FrontendConfigurationProvider + +Neos\Neos\Ui\Domain\InitialData\InitialStateProviderInterface: + className: Neos\Neos\Ui\Infrastructure\Configuration\InitialStateProvider + +Neos\Neos\Ui\Domain\InitialData\MenuProviderInterface: + className: Neos\Neos\Ui\Infrastructure\Neos\MenuProvider + +Neos\Neos\Ui\Domain\InitialData\NodeTypeGroupsAndRolesProviderInterface: + className: Neos\Neos\Ui\Infrastructure\ContentRepository\NodeTypeGroupsAndRolesProvider + +Neos\Neos\Ui\Domain\InitialData\RoutesProviderInterface: + className: Neos\Neos\Ui\Infrastructure\MVC\RoutesProvider + +Neos\Neos\Ui\Infrastructure\Cache\CacheConfigurationVersionProvider: + properties: + configurationCache: + object: + factoryObjectName: Neos\Flow\Cache\CacheManager + factoryMethodName: getCache + arguments: + 1: + value: Neos_Neos_Configuration_Version diff --git a/Configuration/Settings.yaml b/Configuration/Settings.yaml index a5aa25d1cf..d545779a1d 100644 --- a/Configuration/Settings.yaml +++ b/Configuration/Settings.yaml @@ -31,9 +31,6 @@ Neos: Ui: - splashScreen: - partial: 'SplashScreen' - # API: "start 100" and smaller numbers; "no numbers", ... resources: diff --git a/Configuration/Views.yaml b/Configuration/Views.yaml deleted file mode 100644 index 1e29893dac..0000000000 --- a/Configuration/Views.yaml +++ /dev/null @@ -1,7 +0,0 @@ -- - requestFilter: 'isPackage("Neos.Neos.Ui") && isController("Backend")' - viewObjectName: 'Neos\Fusion\View\FusionView' - options: - fusionPathPatterns: - - 'resource://Neos.Neos.Ui/Private/Fusion/Backend' - diff --git a/Resources/Private/Fusion/Backend/Component/ModuleMenu.fusion b/Resources/Private/Fusion/Backend/Component/ModuleMenu.fusion deleted file mode 100644 index 024315ea5a..0000000000 --- a/Resources/Private/Fusion/Backend/Component/ModuleMenu.fusion +++ /dev/null @@ -1,64 +0,0 @@ -prototype(Neos.Neos.Ui:Component.ModuleMenu) < prototype(Neos.Fusion:Map) { - items = ${modulesForMenu} - itemName = 'module' - itemKey = 'moduleName' - - keyRenderer = ${moduleName} - itemRenderer = Neos.Fusion:DataStructure { - @if.moduleNotHidden = ${module.hideInMenu != true} - label = ${module.label} - icon = ${module.icon} - uri = ${module.uri} - target = 'Window' - - children = Neos.Fusion:Case { - sites { - condition = ${module.module == 'content'} - renderer = Neos.Fusion:Map { - items = ${sitesForMenu} - itemName = 'currentSiteInMenu' - iterationName = 'iteration' - - keyRenderer = ${iteration.index} - itemRenderer = Neos.Fusion:DataStructure { - icon = 'globe' - label = ${currentSiteInMenu.name} - uri = ${currentSiteInMenu.uri} - target = 'Window' - isActive = ${currentSiteInMenu.active} - skipI18n = true - } - } - } - - submodules { - condition = ${true} - renderer = Neos.Fusion:Map { - items = ${module.submodules} - itemName = 'submodule' - itemKey = 'submoduleName' - iterationName = 'iteration' - - keyRenderer = ${submoduleName} - itemRenderer = Neos.Fusion:DataStructure { - @if.moduleNotHidden = ${submodule.hideInMenu != true} - icon = ${submodule.icon} - label = ${submodule.label} - uri = ${submodule.uri} - position = ${submodule.position} - isActive = true - target = 'Window' - skipI18n = false - } - - @process.filterHiddenSubmodules = ${Array.filter(value, (x, index) => x != null)} - @process.sort = ${Array.slice(Neos.Ui.PositionalArraySorter.sort(value), 0)} - @process.values = ${Array.values(value)} - } - } - } - } - - @process.filterHiddenModules = ${Array.values(Array.filter(value, (x, index) => x != null))} - @process.json = ${Json.stringify(value)} -} diff --git a/Resources/Private/Fusion/Backend/Root.fusion b/Resources/Private/Fusion/Backend/Root.fusion deleted file mode 100644 index 2374d84821..0000000000 --- a/Resources/Private/Fusion/Backend/Root.fusion +++ /dev/null @@ -1,281 +0,0 @@ -include: resource://Neos.Fusion/Private/Fusion/Root.fusion -include: resource://Neos.Neos/Private/Fusion/Prototypes/NodeUri.fusion -include: resource://Neos.Neos.Ui/Private/Fusion/Prototypes/RenderConfiguration.fusion -include: resource://Neos.Neos.Ui/Private/Fusion/Backend/Component/* - -backend = Neos.Fusion:Template { - templatePath = 'resource://Neos.Neos.Ui/Private/Templates/Backend/Index.html' - - headScripts = ${headScripts} - headStylesheets = ${headStylesheets} - splashScreenPartial = ${splashScreenPartial} - - headIcons = Neos.Fusion:Join { - appleTouchIcon = Neos.Fusion:Tag { - tagName = 'link' - attributes { - href = Neos.Fusion:ResourceUri { - path = 'resource://Neos.Neos.Ui/Public/Images/apple-touch-icon.png' - } - sizes = '180x180' - rel = 'apple-touch-icon' - } - } - favicon16 = Neos.Fusion:Tag { - tagName = 'link' - attributes { - href = Neos.Fusion:ResourceUri { - path = 'resource://Neos.Neos.Ui/Public/Images/favicon-16x16.png' - } - sizes = '16x16' - rel = 'icon' - type = 'image/png' - } - } - favicon32 = Neos.Fusion:Tag { - tagName = 'link' - attributes { - href = Neos.Fusion:ResourceUri { - path = 'resource://Neos.Neos.Ui/Public/Images/favicon-32x32.png' - } - sizes = '32x32' - rel = 'icon' - type = 'image/png' - } - } - safariPinnedTab = Neos.Fusion:Tag { - tagName = 'link' - attributes { - href = Neos.Fusion:ResourceUri { - path = 'resource://Neos.Neos.Ui/Public/Images/safari-pinned-tab.svg' - } - rel = 'mask-icon' - color = '#00adee' - } - } - } - - configuration = Neos.Fusion:DataStructure { - nodeTree = ${Configuration.setting('Neos.Neos.userInterface.navigateComponent.nodeTree')} - structureTree = ${Configuration.setting('Neos.Neos.userInterface.navigateComponent.structureTree')} - allowedTargetWorkspaces = ${Neos.Ui.Workspace.getAllowedTargetWorkspaces(contentRepositoryId)} - endpoints = Neos.Fusion:DataStructure { - nodeTypeSchema = Neos.Fusion:UriBuilder { - package = 'Neos.Neos' - controller = 'Backend\\Schema' - action = 'nodeTypeSchema' - absolute = true - arguments = Neos.Fusion:DataStructure { - # TODO: dirty hack to not have to re-implement neos:backend.configurationCacheVersion VH - version = Neos.Fusion:Template { - templatePath = 'resource://Neos.Neos.Ui/Private/Templates/Backend/ConfigurationVersion.html' - @process.trim = ${String.trim(value)} - } - } - } - translations = Neos.Fusion:UriBuilder { - package = 'Neos.Neos' - controller = 'Backend\\Backend' - action = 'xliffAsJson' - absolute = true - arguments = Neos.Fusion:DataStructure { - locale = ${interfaceLanguage} - - # TODO: dirty hack to not have to re-implement neos:backend.configurationCacheVersion VH - version = Neos.Fusion:Template { - templatePath = 'resource://Neos.Neos.Ui/Private/Templates/Backend/ConfigurationVersion.html' - @process.trim = ${String.trim(value)} - } - } - } - } - @process.json = ${Json.stringify(value)} - } - - routes = Neos.Fusion:DataStructure { - prototype(Neos.Fusion:UriBuilder) { - absolute = true - } - - ui = Neos.Fusion:DataStructure { - service = Neos.Fusion:DataStructure { - prototype(Neos.Fusion:UriBuilder) { - package = 'Neos.Neos.Ui' - controller = 'BackendService' - } - - change = Neos.Fusion:UriBuilder { - action = 'change' - } - publish = Neos.Fusion:UriBuilder { - action = 'publish' - } - discard = Neos.Fusion:UriBuilder { - action = 'discard' - } - changeBaseWorkspace = Neos.Fusion:UriBuilder { - action = 'changeBaseWorkspace' - } - rebaseWorkspace = Neos.Fusion:UriBuilder { - action = 'rebaseWorkspace' - } - copyNodes = Neos.Fusion:UriBuilder { - action = 'copyNodes' - } - cutNodes = Neos.Fusion:UriBuilder { - action = 'cutNodes' - } - clearClipboard = Neos.Fusion:UriBuilder { - action = 'clearClipboard' - } - flowQuery = Neos.Fusion:UriBuilder { - action = 'flowQuery' - } - generateUriPathSegment = Neos.Fusion:UriBuilder { - action = 'generateUriPathSegment' - } - getWorkspaceInfo = Neos.Fusion:UriBuilder { - action = 'getWorkspaceInfo' - } - getAdditionalNodeMetadata = Neos.Fusion:UriBuilder { - action = 'getAdditionalNodeMetadata' - } - } - } - core = Neos.Fusion:DataStructure { - prototype(Neos.Fusion:UriBuilder) { - package = 'Neos.Neos' - } - - content = Neos.Fusion:DataStructure { - prototype(Neos.Fusion:UriBuilder) { - controller = 'Backend\\Content' - } - - imageWithMetadata = Neos.Fusion:UriBuilder { - action = 'imageWithMetaData' - } - createImageVariant = Neos.Fusion:UriBuilder { - action = 'createImageVariant' - } - loadMasterPlugins = Neos.Fusion:UriBuilder { - action = 'masterPlugins' - } - loadPluginViews = Neos.Fusion:UriBuilder { - action = 'pluginViews' - } - uploadAsset = Neos.Fusion:UriBuilder { - action = 'uploadAsset' - } - } - service = Neos.Fusion:DataStructure { - assetProxies = Neos.Fusion:UriBuilder { - controller = 'Service\\AssetProxies' - action = 'index' - } - assets = Neos.Fusion:UriBuilder { - controller = 'Service\\Assets' - action = 'index' - } - nodes = Neos.Fusion:UriBuilder { - controller = 'Service\\Nodes' - action = 'index' - } - userPreferences = Neos.Fusion:UriBuilder { - subpackage = 'Service' - controller = 'UserPreference' - action = 'index' - format = 'json' - } - dataSource = Neos.Fusion:UriBuilder { - subpackage = 'Service' - controller = 'DataSource' - action = 'index' - format = 'json' - } - contentDimensions = Neos.Fusion:UriBuilder { - package = 'Neos.Neos' - controller = 'Service\\ContentDimensions' - action = 'index' - } - impersonateStatus = Neos.Fusion:UriBuilder { - package = 'Neos.Neos' - controller = 'Backend\\Impersonate' - action = 'status' - format = 'json' - } - impersonateRestore = Neos.Fusion:UriBuilder { - package = 'Neos.Neos' - controller = 'Backend\\Impersonate' - action = 'restoreWithResponse' - format = 'json' - } - } - modules = Neos.Fusion:DataStructure { - prototype(Neos.Fusion:UriBuilder) { - controller = 'Backend\\Module' - } - workspaces = Neos.Fusion:UriBuilder { - action = 'index' - arguments { - module = 'management/workspaces' - } - } - userSettings = Neos.Fusion:UriBuilder { - controller = 'Backend\\Module' - action = 'index' - arguments { - module = 'user/usersettings' - } - } - mediaBrowser = Neos.Fusion:UriBuilder { - controller = 'Backend\\Module' - action = 'index' - arguments { - module = 'media/browser' - } - } - } - login = Neos.Fusion:UriBuilder { - controller = 'Login' - action = 'index' - format = 'json' - } - logout = Neos.Fusion:UriBuilder { - controller = 'Login' - action = 'logout' - } - } - @process.json = ${Json.stringify(value)} - } - - frontendConfiguration = Neos.Neos.Ui:RenderConfiguration { - path = 'frontendConfiguration' - @process.json = ${Json.stringify(value)} - } - - nodeTypes = Neos.Fusion:DataStructure { - roles = ${Configuration.setting('Neos.Neos.Ui.nodeTypeRoles')} - groups = ${Neos.Ui.PositionalArraySorter.sort(Configuration.setting('Neos.Neos.nodeTypes.groups'))} - - @process.json = ${Json.stringify(value)} - } - - menu = Neos.Neos.Ui:Component.ModuleMenu - - initialState = Neos.Neos.Ui:RenderConfiguration { - path = 'initialState' - context { - contentRepositoryId = ${contentRepositoryId} - documentNode = ${documentNode} - site = ${site} - user = ${user} - clipboardNodes = ${clipboardNodes} - clipboardMode = ${clipboardMode} - } - - @process.json = ${Json.stringify(value)} - } - - env = ${Configuration.setting('Neos.Flow.core.context')} -} diff --git a/Resources/Private/Templates/Backend/Index.html b/Resources/Private/Templates/Backend/Index.html deleted file mode 100644 index 2305dc53e8..0000000000 --- a/Resources/Private/Templates/Backend/Index.html +++ /dev/null @@ -1,24 +0,0 @@ - -{namespace neos=Neos\Neos\ViewHelpers} - - - - Neos CMS - - - - - - - - - {headStylesheets -> f:format.raw()} - {headScripts -> f:format.raw()} - {headIcons -> f:format.raw()} - - -
- -
- - diff --git a/Resources/Private/Templates/Backend/Partials/SplashScreen.html b/Resources/Private/Templates/Backend/Partials/SplashScreen.html deleted file mode 100644 index 9d519f72a7..0000000000 --- a/Resources/Private/Templates/Backend/Partials/SplashScreen.html +++ /dev/null @@ -1,33 +0,0 @@ - -
- -
diff --git a/packages/neos-ui-redux-store/src/System/index.spec.js b/packages/neos-ui-redux-store/src/System/index.spec.js index 7057c686b3..4263d920b9 100644 --- a/packages/neos-ui-redux-store/src/System/index.spec.js +++ b/packages/neos-ui-redux-store/src/System/index.spec.js @@ -2,7 +2,6 @@ import {actionTypes, actions, reducer, defaultState, selectors} from './index'; test(`should export actionTypes`, () => { expect(actionTypes).not.toBe(undefined); - expect(typeof (actionTypes.BOOT)).toBe('string'); expect(typeof (actionTypes.INIT)).toBe('string'); expect(typeof (actionTypes.READY)).toBe('string'); expect(typeof (actionTypes.AUTHENTICATION_TIMEOUT)).toBe('string'); @@ -11,13 +10,11 @@ test(`should export actionTypes`, () => { test(`should export action creators`, () => { expect(actions).not.toBe(undefined); - expect(typeof (actions.boot)).toBe('function'); expect(typeof (actions.init)).toBe('function'); expect(typeof (actions.ready)).toBe('function'); expect(typeof (actions.authenticationTimeout)).toBe('function'); expect(typeof (actions.reauthenticationSucceeded)).toBe('function'); - expect(typeof (actions.boot().type)).toBe('string'); expect(typeof (actions.init().type)).toBe('string'); expect(typeof (actions.ready().type)).toBe('string'); expect(typeof (actions.authenticationTimeout().type)).toBe('string'); diff --git a/packages/neos-ui-redux-store/src/System/index.ts b/packages/neos-ui-redux-store/src/System/index.ts index 2884cf45c5..e8ad93b036 100644 --- a/packages/neos-ui-redux-store/src/System/index.ts +++ b/packages/neos-ui-redux-store/src/System/index.ts @@ -27,7 +27,6 @@ export const defaultState: State = { // Export the action types // export enum actionTypes { - BOOT = '@neos/neos-ui/System/BOOT', INIT = '@neos/neos-ui/System/INIT', READY = '@neos/neos-ui/System/READY', AUTHENTICATION_TIMEOUT = '@neos/neos-ui/System/AUTHENTICATION_TIMEOUT', @@ -38,7 +37,6 @@ export enum actionTypes { // Export the actions // export const actions = { - boot: () => createAction(actionTypes.BOOT), init: (state: GlobalState) => createAction(actionTypes.INIT, state), ready: () => createAction(actionTypes.READY), authenticationTimeout: () => createAction(actionTypes.AUTHENTICATION_TIMEOUT), diff --git a/packages/neos-ui/src/System/index.js b/packages/neos-ui/src/System/index.js index a6b93ef92e..fca0607032 100644 --- a/packages/neos-ui/src/System/index.js +++ b/packages/neos-ui/src/System/index.js @@ -1,65 +1,93 @@ -import {discover} from '@neos-project/utils-helpers'; import {initializeJsAPI} from '@neos-project/neos-ui-backend-connector'; import fetchWithErrorHandling from '@neos-project/neos-ui-backend-connector/src/FetchWithErrorHandling/index'; -const getInlinedData = dataName => { - return new Promise((resolve, reject) => { - const result = window['_NEOS_UI_' + dataName]; - delete window['_NEOS_UI_' + dataName]; - try { - resolve(result); - } catch (ex) { - reject(ex); - } - }); -}; - -export const getAppContainer = discover(function * () { - const appContainer = yield new Promise(resolve => { - document.addEventListener('DOMContentLoaded', () => { - resolve(document.getElementById('appContainer')); - }); - }); - - return appContainer; -}); - -export const getCsrfToken = discover(function * () { - const appContainer = yield getAppContainer; +import {terminateDueToFatalInitializationError} from './terminateDueToFatalInitializationError'; - return appContainer.dataset.csrfToken; -}); - -export const getSystemEnv = discover(function * () { - const appContainer = yield getAppContainer; - - return appContainer.dataset.env; -}); +let initialData = null; +function parseInitialData() { + if (initialData) { + return initialData; + } -export const getServerState = getInlinedData('initialState'); + const initialDataContainer = document.getElementById('initialData'); + if (!initialDataContainer) { + return terminateDueToFatalInitializationError(` +

This page is missing a <script/>-container with the + id #initialData.

+ `); + } -export const getConfiguration = getInlinedData('configuration'); + try { + const initialDataAsJson = initialDataContainer.innerText; + initialData = JSON.parse(initialDataAsJson); -export const getNodeTypes = getInlinedData('nodeTypes'); - -export const getFrontendConfiguration = getInlinedData('frontendConfiguration'); - -export const getRoutes = getInlinedData('routes'); - -export const getMenu = getInlinedData('menu'); - -export const getNeos = discover(function * () { - const csrfToken = yield getCsrfToken; - - fetchWithErrorHandling.setCsrfToken(csrfToken); - - const systemEnv = yield getSystemEnv; - const routes = yield getRoutes; - - const neos = initializeJsAPI(window, { - systemEnv, - routes - }); + if (typeof initialData === 'object' && initialData) { + return initialData; + } - return neos; + return terminateDueToFatalInitializationError(` +

JSON-content of #initialData has an unexpected + type: ${typeof initialData}

+ `); + } catch (err) { + return terminateDueToFatalInitializationError(` +

JSON.parse for content of #initialData failed: + ${err}

+ `); + } +} + +function getInlinedData(dataName) { + const initialData = parseInitialData(); + + if (dataName in initialData) { + return initialData[dataName]; + } + + return terminateDueToFatalInitializationError(` +

Initial data for ${dataName} could not + be read from #initialData container.

+ `); +} + +export const appContainer = document.getElementById('appContainer'); +if (!appContainer) { + terminateDueToFatalInitializationError(` +

This page is missing a container with the id #appContainer.

+ `); +} + +export const {csrfToken} = appContainer.dataset; +if (!csrfToken) { + terminateDueToFatalInitializationError(` +

The container with the id #appContainer is missing an attribute + data-csrf-token.

+ `); +} + +fetchWithErrorHandling.setCsrfToken(csrfToken); + +export const {env: systemEnv} = appContainer.dataset; +if (!systemEnv) { + terminateDueToFatalInitializationError(` +

The container with the id #appContainer is missing an attribute + data-env (eg. Production, Development, etc...).

+ `); +} + +export const serverState = getInlinedData('initialState'); + +export const configuration = getInlinedData('configuration'); + +export const nodeTypes = getInlinedData('nodeTypes'); + +export const frontendConfiguration = getInlinedData('frontendConfiguration'); + +export const routes = getInlinedData('routes'); + +export const menu = getInlinedData('menu'); + +export const neos = initializeJsAPI(window, { + systemEnv, + routes }); diff --git a/packages/neos-ui/src/System/terminateDueToFatalInitializationError.js b/packages/neos-ui/src/System/terminateDueToFatalInitializationError.js new file mode 100644 index 0000000000..5edaecc5ac --- /dev/null +++ b/packages/neos-ui/src/System/terminateDueToFatalInitializationError.js @@ -0,0 +1,24 @@ +import logo from '@neos-project/react-ui-components/src/Logo/logo.svg'; + +import styles from '../Containers/ErrorBoundary/style.module.css'; + +export function terminateDueToFatalInitializationError(reason) { + if (!document.body) { + throw new Error(reason); + } + + document.title = 'The Neos UI could not be initialized.'; + document.body.innerHTML = ` +
+
+ Neos +

+ Sorry, but the Neos UI could not be initialized. +

+ ${reason} +
+
+ `; + + throw new Error(document.body.innerText); +} diff --git a/packages/neos-ui/src/index.js b/packages/neos-ui/src/index.js index 071de0111b..4908d5849e 100644 --- a/packages/neos-ui/src/index.js +++ b/packages/neos-ui/src/index.js @@ -2,18 +2,24 @@ import React from 'react'; import ReactDOM from 'react-dom'; import {createStore, applyMiddleware, compose} from 'redux'; import createSagaMiddleware from 'redux-saga'; -import {put, select, all} from 'redux-saga/effects'; import merge from 'lodash.merge'; import {actions} from '@neos-project/neos-ui-redux-store'; import {createConsumerApi} from '@neos-project/neos-ui-extensibility'; import fetchWithErrorHandling from '@neos-project/neos-ui-backend-connector/src/FetchWithErrorHandling'; import {SynchronousMetaRegistry} from '@neos-project/neos-ui-extensibility/src/registry'; -import {delay} from '@neos-project/utils-helpers'; import backend from '@neos-project/neos-ui-backend-connector'; import {handleActions} from '@neos-project/utils-redux'; -import * as system from './System'; +import { + appContainer, + frontendConfiguration, + configuration, + routes, + serverState, + menu, + nodeTypes +} from './System'; import localStorageMiddleware from './localStorageMiddleware'; import clipboardMiddleware from './clipboardMiddleware'; import Root from './Containers/Root'; @@ -47,112 +53,61 @@ require('@neos-project/neos-ui-validators/src/manifest'); require('@neos-project/neos-ui-i18n/src/manifest'); require('@neos-project/neos-ui-sagas/src/manifest'); -// -// The main application -// -function * application() { - const appContainer = yield system.getAppContainer; +async function main() { + initializePlugins(); + initializeFrontendConfiguration(); + initializeAdditionalReduxReducers(); + initializeAdditionalReduxSagas(); + initializeReduxState(); + initializeFetchWithErrorHandling(); - // - // Initialize Neos JS API - // - yield system.getNeos; + await Promise.all([ + loadNodeTypesSchema(), + loadTranslations(), + loadImpersonateStatus() + ]); - // - // Load frontend configuration very early, as we want to make it available in manifests - // - const frontendConfiguration = yield system.getFrontendConfiguration; + store.dispatch(actions.System.ready()); - const configuration = yield system.getConfiguration; + renderApplication(); + reloadNodes(); +} + +function initializeFrontendConfiguration() { + const frontendConfigurationRegistry = globalRegistry.get('frontendConfiguration'); - const routes = yield system.getRoutes; + Object.keys(frontendConfiguration).forEach(key => { + frontendConfigurationRegistry.set(key, { + ...frontendConfiguration[key] + }); + }); +} - // - // Initialize extensions - // +function initializePlugins() { manifests .map(manifest => manifest[Object.keys(manifest)[0]]) .forEach(({bootstrap}) => bootstrap(globalRegistry, {store, frontendConfiguration, configuration, routes})); +} +function initializeAdditionalReduxReducers() { const reducers = globalRegistry.get('reducers').getAllAsList().map(element => element.reducer); delegatingReducer.setReducer(handleActions(reducers)); +} - // - // Bootstrap the saga middleware with initial sagas - // +function initializeAdditionalReduxSagas() { globalRegistry.get('sagas').getAllAsList().forEach(element => sagaMiddleWare.run(element.saga, {store, globalRegistry, configuration})); +} - // - // Tell everybody, that we're booting now - // - store.dispatch(actions.System.boot()); - - const {getJsonResource, impersonateStatus} = backend.get().endpoints; - - const groupsAndRoles = yield system.getNodeTypes; - - // - // Load json resources - // - const nodeTypesSchemaPromise = getJsonResource(configuration.endpoints.nodeTypeSchema); - const translationsPromise = getJsonResource(configuration.endpoints.translations); - - // Fire multiple async requests in parallel - const [nodeTypesSchema, translations] = yield all([nodeTypesSchemaPromise, translationsPromise]); - const nodeTypesRegistry = globalRegistry.get('@neos-project/neos-ui-contentrepository'); - Object.keys(nodeTypesSchema.nodeTypes).forEach(nodeTypeName => { - nodeTypesRegistry.set(nodeTypeName, { - ...nodeTypesSchema.nodeTypes[nodeTypeName], - name: nodeTypeName - }); - }); - nodeTypesRegistry.setConstraints(nodeTypesSchema.constraints); - nodeTypesRegistry.setInheritanceMap(nodeTypesSchema.inheritanceMap); - nodeTypesRegistry.setGroups(groupsAndRoles.groups); - nodeTypesRegistry.setRoles(groupsAndRoles.roles); - - // - // Load translations - // - const i18nRegistry = globalRegistry.get('i18n'); - i18nRegistry.setTranslations(translations); - - const frontendConfigurationRegistry = globalRegistry.get('frontendConfiguration'); - - Object.keys(frontendConfiguration).forEach(key => { - frontendConfigurationRegistry.set(key, { - ...frontendConfiguration[key] - }); - }); - - // - // Hydrate server state - // Deep merges state fetched from server with the state saved in the local storage - // - const serverState = yield system.getServerState; - const persistedState = localStorage.getItem('persistedState') ? JSON.parse(localStorage.getItem('persistedState')) : {}; +function initializeReduxState() { + const persistedState = localStorage.getItem('persistedState') + ? JSON.parse(localStorage.getItem('persistedState')) + : {}; const mergedState = merge({}, serverState, persistedState); - yield put(actions.System.init(mergedState)); - try { - const impersonateState = yield impersonateStatus(); - if (impersonateState) { - yield put(actions.User.Impersonate.fetchStatus(impersonateState)); - } - } catch (error) { - store.dispatch(actions.UI.FlashMessages.add('impersonateStatusError', error.message, 'error')); - } - - // - // Just make sure that everybody does their initialization homework - // - yield delay(0); - - // - // Inform everybody, that we're ready now - // - yield put(actions.System.ready()); + store.dispatch(actions.System.init(mergedState)); +} +function initializeFetchWithErrorHandling() { fetchWithErrorHandling.registerAuthenticationErrorHandler(() => { store.dispatch(actions.System.authenticationTimeout()); }); @@ -188,12 +143,49 @@ function * application() { store.dispatch(actions.UI.FlashMessages.add('fetch error', message, 'error')); }); +} + +async function loadNodeTypesSchema() { + const {getJsonResource} = backend.get().endpoints; + const nodeTypesRegistry = globalRegistry.get('@neos-project/neos-ui-contentrepository'); + + const nodeTypesSchema = await getJsonResource(configuration.endpoints.nodeTypeSchema); + Object.keys(nodeTypesSchema.nodeTypes).forEach(nodeTypeName => { + nodeTypesRegistry.set(nodeTypeName, { + ...nodeTypesSchema.nodeTypes[nodeTypeName], + name: nodeTypeName + }); + }); - const menu = yield system.getMenu; + nodeTypesRegistry.setConstraints(nodeTypesSchema.constraints); + nodeTypesRegistry.setInheritanceMap(nodeTypesSchema.inheritanceMap); - // - // After everything was initilalized correctly, render the application itself. - // + const {groups, roles} = nodeTypes; + nodeTypesRegistry.setGroups(groups); + nodeTypesRegistry.setRoles(roles); +} + +async function loadTranslations() { + const {getJsonResource} = backend.get().endpoints; + const i18nRegistry = globalRegistry.get('i18n'); + const translations = await getJsonResource(configuration.endpoints.translations); + + i18nRegistry.setTranslations(translations); +} + +async function loadImpersonateStatus() { + try { + const {impersonateStatus} = backend.get().endpoints; + const impersonateState = await impersonateStatus(); + if (impersonateState) { + store.dispatch(actions.User.Impersonate.fetchStatus(impersonateState)); + } + } catch (error) { + store.dispatch(actions.UI.FlashMessages.add('impersonateStatusError', error.message, 'error')); + } +} + +function renderApplication() { ReactDOM.render( , appContainer ); +} - const siteNodeContextPath = yield select( - state => state?.cr?.nodes?.siteNode - ); - const documentNodeContextPath = yield select( - state => state?.cr?.nodes?.documentNode - ); - yield put(actions.CR.Nodes.reloadState({ +function reloadNodes() { + const state = store.getState(); + const siteNodeContextPath = state?.cr?.nodes?.siteNode; + const documentNodeContextPath = state?.cr?.nodes?.documentNode; + + store.dispatch(actions.CR.Nodes.reloadState({ siteNodeContextPath, documentNodeContextPath, merge: true })); } -sagaMiddleWare.run(application); +document.addEventListener('DOMContentLoaded', main); diff --git a/packages/utils-helpers/src/delay.spec.js b/packages/utils-helpers/src/delay.spec.js deleted file mode 100644 index 1c1a156a20..0000000000 --- a/packages/utils-helpers/src/delay.spec.js +++ /dev/null @@ -1,11 +0,0 @@ -import delay from './delay'; - -test(`should export a function`, () => { - expect(typeof delay).toBe('function'); -}); - -test(`should call the given function after the given delay`, () => { - const promise = delay(200); - - expect(promise instanceof Promise).toBe(true); -}); diff --git a/packages/utils-helpers/src/delay.ts b/packages/utils-helpers/src/delay.ts deleted file mode 100644 index 83a4a9192f..0000000000 --- a/packages/utils-helpers/src/delay.ts +++ /dev/null @@ -1,9 +0,0 @@ -// -// Provides a promise that resolves after -// timeInMilliseconds milliseconds -// -// ToDo: We could use `redux-saga`'s delay function instead of writing our own implementation. -// -export default function delay(timeInMilliseconds: number): Promise { - return new Promise(resolve => setTimeout(resolve, timeInMilliseconds)); -} diff --git a/packages/utils-helpers/src/discover.spec.js b/packages/utils-helpers/src/discover.spec.js deleted file mode 100644 index dae169f8dc..0000000000 --- a/packages/utils-helpers/src/discover.spec.js +++ /dev/null @@ -1,17 +0,0 @@ -import discover from './discover'; - -test(`should export a function`, () => { - expect(typeof discover).toBe('function'); -}); - -test(`should transform a generator function into a function that returns a Promise`, async () => { - function * gen() { - yield 1; - yield 2; - yield 3; - return 4; - } - const result = await discover(gen); - - expect(result).toBe(4); -}); diff --git a/packages/utils-helpers/src/discover.ts b/packages/utils-helpers/src/discover.ts deleted file mode 100644 index 5b84b3897a..0000000000 --- a/packages/utils-helpers/src/discover.ts +++ /dev/null @@ -1,25 +0,0 @@ -// -// Turns a generator function into a promise, -// helpful to encapsulate side effects -// -const handle = (generator: Generator, result: any): Promise => { - if (result.done) { - return Promise.resolve(result.value); - } - return Promise.resolve(result.value).then( - res => handle(generator, generator.next(res)), - err => handle(generator, generator.throw && generator.throw(err)) - ); -}; - -export default function discover(generatorFn: GeneratorFunction): Promise { - return new Promise((resolve, reject) => { - const generator = generatorFn(); - - try { - resolve(handle(generator, generator.next())); - } catch (ex) { - reject(ex); - } - }); -} diff --git a/packages/utils-helpers/src/index.ts b/packages/utils-helpers/src/index.ts index ba97471a21..c12402561c 100644 --- a/packages/utils-helpers/src/index.ts +++ b/packages/utils-helpers/src/index.ts @@ -1,5 +1,3 @@ -import delay from './delay'; -import discover from './discover'; import isThenable from './isThenable'; import {stripTags, stripTagsEncoded} from './stripTags'; import decodeHtml from './decodeHtml'; @@ -12,8 +10,6 @@ import isEqualSet from './isEqualSet'; import isNil from './isNil'; export { - delay, - discover, decodeHtml, getVersion, isThenable,