+ */
+ 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 = '';
+ $result .= '';
+ $result .= '';
+ $result .= $this->renderHead();
+ $result .= '';
+ $result .= '';
+ $result .= $this->renderBody();
+ $result .= '';
+ $result .= '';
+
+ return $result;
+ }
+
+ private function renderLang(): string
+ {
+ return $this->userService->getInterfaceLanguage();
+ }
+
+ private function renderHead(): string
+ {
+ $result = '';
+ $result .= '';
+
+ $result .= '' . $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 = `
+
+
+
+
+ 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,