diff --git a/Classes/Controller/BackendController.php b/Classes/Controller/BackendController.php index 426c6c1be6..143af5b420 100644 --- a/Classes/Controller/BackendController.php +++ b/Classes/Controller/BackendController.php @@ -21,16 +21,17 @@ use Neos\Neos\Domain\Repository\DomainRepository; use Neos\Neos\Domain\Repository\SiteRepository; use Neos\Neos\Domain\Service\NodeTypeNameFactory; +use Neos\Neos\Domain\Service\UserService; use Neos\Neos\Domain\Service\WorkspaceService; use Neos\Neos\FrontendRouting\NodeUriBuilderFactory; use Neos\Neos\FrontendRouting\SiteDetection\SiteDetectionResult; -use Neos\Neos\Service\UserService; 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\Domain\InitialData\UserProviderInterface; use Neos\Neos\Ui\Presentation\ApplicationView; /** @@ -45,12 +46,6 @@ class BackendController extends ActionController protected $defaultViewObjectName = ApplicationView::class; - /** - * @Flow\Inject - * @var UserService - */ - protected $userService; - /** * @Flow\Inject * @var DomainRepository @@ -105,6 +100,18 @@ class BackendController extends ActionController */ protected $menuProvider; + /** + * @Flow\Inject + * @var UserProviderInterface + */ + protected $userProvider; + + /** + * @Flow\Inject + * @var UserService + */ + protected $userService; + /** * @Flow\Inject * @var InitialStateProviderInterface @@ -135,7 +142,7 @@ public function indexAction(string $node = null) $contentRepository = $this->contentRepositoryRegistry->get($siteDetectionResult->contentRepositoryId); $nodeAddress = $node !== null ? NodeAddress::fromJsonString($node) : null; - $user = $this->userService->getBackendUser(); + $user = $this->userService->getCurrentUser(); if ($user === null) { $this->redirectToUri($this->uriBuilder->uriFor('index', [], 'Login', 'Neos.Neos')); @@ -175,6 +182,12 @@ public function indexAction(string $node = null) $node = $subgraph->findNodeById($nodeAddress->aggregateId); } + // todo duplicate user logic see above $this->userService->getBackendUser() is never null here ... pass the user instead to the method? + $user = $this->userProvider->getUser(); + if (!$user) { + $this->redirectToUri($this->uriBuilder->uriFor('index', [], 'Login', 'Neos.Neos')); + } + $this->view->setOption('title', 'Neos CMS'); $this->view->assign('initialData', [ 'configuration' => @@ -196,12 +209,12 @@ public function indexAction(string $node = null) $this->menuProvider->getMenu( actionRequest: $this->request, ), + 'user' => $user, 'initialState' => $this->initialStateProvider->getInitialState( actionRequest: $this->request, documentNode: $node, site: $siteNode, - user: $user, ), ]); } diff --git a/Classes/Domain/InitialData/InitialStateProviderInterface.php b/Classes/Domain/InitialData/InitialStateProviderInterface.php index 40b7c0b5af..ed433b4f1d 100644 --- a/Classes/Domain/InitialData/InitialStateProviderInterface.php +++ b/Classes/Domain/InitialData/InitialStateProviderInterface.php @@ -32,6 +32,5 @@ public function getInitialState( ActionRequest $actionRequest, ?Node $documentNode, ?Node $site, - User $user, ): array; } diff --git a/Classes/Domain/InitialData/MenuProviderInterface.php b/Classes/Domain/InitialData/MenuProviderInterface.php index 34f4b8355a..26aa2be808 100644 --- a/Classes/Domain/InitialData/MenuProviderInterface.php +++ b/Classes/Domain/InitialData/MenuProviderInterface.php @@ -25,7 +25,7 @@ interface MenuProviderInterface { /** - * @return array}> + * @return array}> */ public function getMenu(ActionRequest $actionRequest): array; } diff --git a/Classes/Domain/InitialData/UserProviderInterface.php b/Classes/Domain/InitialData/UserProviderInterface.php new file mode 100644 index 0000000000..ae3f3c19ff --- /dev/null +++ b/Classes/Domain/InitialData/UserProviderInterface.php @@ -0,0 +1,29 @@ +configurationRenderingService->computeConfiguration( $this->initialStateBeforeProcessing, @@ -51,7 +50,6 @@ public function getInitialState( 'request' => $actionRequest, 'documentNode' => $documentNode, 'site' => $site, - 'user' => $user, 'clipboardNodes' => $this->clipboard->getSerializedNodeAddresses(), 'clipboardMode' => $this->clipboard->getMode(), ] diff --git a/Classes/Infrastructure/Neos/MenuProvider.php b/Classes/Infrastructure/Neos/MenuProvider.php index ae8273cfcb..cf75f55659 100644 --- a/Classes/Infrastructure/Neos/MenuProvider.php +++ b/Classes/Infrastructure/Neos/MenuProvider.php @@ -55,7 +55,6 @@ public function getMenu(ActionRequest $actionRequest): array $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), @@ -67,7 +66,7 @@ public function getMenu(ActionRequest $actionRequest): array } /** - * @return array + * @return array */ private function buildChildrenForSites(ControllerContext $controllerContext): array { @@ -89,7 +88,6 @@ private function buildChildrenForSites(ControllerContext $controllerContext): ar $result[$index]['icon'] = 'globe'; $result[$index]['label'] = $name; $result[$index]['uri'] = $uri; - $result[$index]['target'] = 'Window'; $result[$index]['isActive'] = $active; $result[$index]['skipI18n'] = true; } @@ -114,7 +112,6 @@ private function buildChildrenForBackendModule(array $module): array $result[$submoduleName]['uri'] = $submodule['uri']; $result[$submoduleName]['position'] = $submodule['position']; $result[$submoduleName]['isActive'] = true; - $result[$submoduleName]['target'] = 'Window'; $result[$submoduleName]['skipI18n'] = false; } diff --git a/Classes/Infrastructure/Neos/UserProvider.php b/Classes/Infrastructure/Neos/UserProvider.php new file mode 100644 index 0000000000..7ec0ac0668 --- /dev/null +++ b/Classes/Infrastructure/Neos/UserProvider.php @@ -0,0 +1,55 @@ +userService->getBackendUser(); + if ($user === null) { + return null; + } + + return [ + 'name' => [ + 'title' => $user->getName()->getTitle(), + 'firstName' => $user->getName()->getFirstName(), + 'middleName' => $user->getName()->getMiddleName(), + 'lastName' => $user->getName()->getLastName(), + 'otherName' => $user->getName()->getOtherName(), + 'fullName' => $user->getName()->getFullName(), + ], + 'preferences' => [ + 'interfaceLanguage' => $user->getPreferences() + ->getInterfaceLanguage(), + ], + ]; + } +} diff --git a/Configuration/Objects.yaml b/Configuration/Objects.yaml index 9c055c11f9..93b1bb6f01 100644 --- a/Configuration/Objects.yaml +++ b/Configuration/Objects.yaml @@ -16,6 +16,9 @@ Neos\Neos\Ui\Domain\InitialData\InitialStateProviderInterface: Neos\Neos\Ui\Domain\InitialData\MenuProviderInterface: className: Neos\Neos\Ui\Infrastructure\Neos\MenuProvider +Neos\Neos\Ui\Domain\InitialData\UserProviderInterface: + className: Neos\Neos\Ui\Infrastructure\Neos\UserProvider + Neos\Neos\Ui\Domain\InitialData\NodeTypeGroupsAndRolesProviderInterface: className: Neos\Neos\Ui\Infrastructure\ContentRepository\NodeTypeGroupsAndRolesProvider diff --git a/packages/neos-ui-backend-connector/src/index.ts b/packages/neos-ui-backend-connector/src/index.ts index 66fe06618a..958258bb2b 100644 --- a/packages/neos-ui-backend-connector/src/index.ts +++ b/packages/neos-ui-backend-connector/src/index.ts @@ -21,7 +21,7 @@ export const define = (parent: {[propName: string]: any}) => (name: string, valu // // Initializes the Neos API // -export const initializeJsAPI = (parent: {[propName: string]: any}, {alias = 'neos', systemEnv = 'Development', routes}: {alias: string, systemEnv: string, routes: Routes}) => { +export const initializeJsAPI = (parent: {[propName: string]: any}, {alias = 'neos', systemEnv = 'Development', routes}: {alias?: string, systemEnv: string, routes: Routes}) => { if (parent[alias] !== undefined) { throw new Error(`Could not initialize Neos API, because ${alias} is already defined.`); } diff --git a/packages/neos-ui-error/src/container/ErrorBoundary/terminateDueToFatalInitializationError.ts b/packages/neos-ui-error/src/container/ErrorBoundary/terminateDueToFatalInitializationError.ts index 6ccb8cefb4..bb38e31b59 100644 --- a/packages/neos-ui-error/src/container/ErrorBoundary/terminateDueToFatalInitializationError.ts +++ b/packages/neos-ui-error/src/container/ErrorBoundary/terminateDueToFatalInitializationError.ts @@ -1,7 +1,7 @@ import logoSvg from '@neos-project/react-ui-components/src/Logo/resource/logo.svg'; import styles from './style.module.css'; -export function terminateDueToFatalInitializationError(reason: string): void { +export function terminateDueToFatalInitializationError(reason: string): never { if (!document.body) { throw new Error(reason); } diff --git a/packages/neos-ui/package.json b/packages/neos-ui/package.json index 3cf2a54b85..90de80bade 100644 --- a/packages/neos-ui/package.json +++ b/packages/neos-ui/package.json @@ -17,6 +17,8 @@ "@fortawesome/free-regular-svg-icons": "^6.5.2", "@fortawesome/free-solid-svg-icons": "^6.5.2", "@friendsofreactjs/react-css-themr": "~4.2.0", + "@neos-project/framework-observable": "workspace:*", + "@neos-project/framework-observable-react": "workspace:*", "@neos-project/neos-ts-interfaces": "workspace:*", "@neos-project/neos-ui-backend-connector": "workspace:*", "@neos-project/neos-ui-ckeditor5-bindings": "workspace:*", diff --git a/packages/neos-ui/src/Containers/Drawer/Drawer.tsx b/packages/neos-ui/src/Containers/Drawer/Drawer.tsx new file mode 100644 index 0000000000..325ac24999 --- /dev/null +++ b/packages/neos-ui/src/Containers/Drawer/Drawer.tsx @@ -0,0 +1,121 @@ +/* + * This file is part of the Neos.Neos.Ui package. + * + * (c) Contributors of the Neos Project - www.neos.io + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ +import React from 'react'; +import mergeClassNames from 'classnames'; + +import {neos} from '@neos-project/neos-ui-decorators'; +import {SynchronousRegistry} from '@neos-project/neos-ui-extensibility'; +import {createState} from '@neos-project/framework-observable'; +import {useLatestState} from '@neos-project/framework-observable-react'; + +import MenuItemGroup from './MenuItemGroup/index'; +import style from './style.module.css'; +import {THRESHOLD_MOUSE_LEAVE} from './constants'; + +const withNeosGlobals = neos(globalRegistry => ({ + containerRegistry: globalRegistry.get('containers') +})); + +export const drawer$ = createState({ + isHidden: true, + collapsedMenuGroups: [] as string[] +}); + +export function toggleDrawer() { + drawer$.update((state) => ({ + ...state, + isHidden: !state.isHidden + })); +} + +function hideDrawer() { + drawer$.update((state) => ({ + ...state, + isHidden: true + })); +} + +function toggleMenuGroup(menuGroupId: string) { + drawer$.update((state) => ({ + ...state, + collapsedMenuGroups: state.collapsedMenuGroups.includes(menuGroupId) + ? state.collapsedMenuGroups.filter((m) => m !== menuGroupId) + : [...state.collapsedMenuGroups, menuGroupId] + })); +} + +const StatelessDrawer: React.FC<{ + containerRegistry: SynchronousRegistry; + + menuData: { + icon?: string; + label: string; + uri: string; + target?: string; + + children: { + icon?: string; + label: string; + uri?: string; + isActive: boolean; + skipI18n: boolean; + }[]; + }[]; +}> = (props) => { + const {isHidden, collapsedMenuGroups} = useLatestState(drawer$); + const mouseLeaveTimeoutRef = React.useRef>(null); + const handleMouseEnter = React.useCallback(() => { + if (mouseLeaveTimeoutRef.current) { + clearTimeout(mouseLeaveTimeoutRef.current); + mouseLeaveTimeoutRef.current = null; + } + }, []); + const handleMouseLeave = React.useCallback(() => { + if (!mouseLeaveTimeoutRef.current) { + mouseLeaveTimeoutRef.current = setTimeout(() => { + hideDrawer(); + mouseLeaveTimeoutRef.current = null; + }, THRESHOLD_MOUSE_LEAVE); + } + }, []); + const {menuData, containerRegistry} = props; + const classNames = mergeClassNames({ + [style.drawer]: true, + [style['drawer--isHidden']]: isHidden + }); + + const BottomComponents = containerRegistry.getChildren('Drawer/Bottom'); + + return ( +
+
+ {Object.entries(menuData).map(([menuGroup, menuGroupConfiguration]) => ( + + ))} +
+
+ {BottomComponents.map((Item, key) => )} +
+
+ ); +} + +export const Drawer = withNeosGlobals(StatelessDrawer as any); diff --git a/packages/neos-ui/src/Containers/Drawer/MenuItem/MenuItem.tsx b/packages/neos-ui/src/Containers/Drawer/MenuItem/MenuItem.tsx new file mode 100644 index 0000000000..50b302bec9 --- /dev/null +++ b/packages/neos-ui/src/Containers/Drawer/MenuItem/MenuItem.tsx @@ -0,0 +1,40 @@ +/* + * This file is part of the Neos.Neos.Ui package. + * + * (c) Contributors of the Neos Project - www.neos.io + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ +import React from 'react'; + +import {Icon, Button} from '@neos-project/react-ui-components'; + +import I18n from '@neos-project/neos-ui-i18n'; + +import style from '../style.module.css'; + +export const MenuItem: React.FC<{ + icon?: string; + label: string; + uri?: string; + isActive: boolean; + skipI18n?: boolean; +}> = (props) => { + const {skipI18n, label, icon, uri} = props; + + return ( + + + + ); +} diff --git a/packages/neos-ui/src/Containers/Drawer/MenuItem/index.js b/packages/neos-ui/src/Containers/Drawer/MenuItem/index.js deleted file mode 100644 index 0db21ad763..0000000000 --- a/packages/neos-ui/src/Containers/Drawer/MenuItem/index.js +++ /dev/null @@ -1,50 +0,0 @@ -import React, {PureComponent} from 'react'; -import PropTypes from 'prop-types'; - -import {Icon, Button} from '@neos-project/react-ui-components'; - -import I18n from '@neos-project/neos-ui-i18n'; - -import style from '../style.module.css'; -import {TARGET_WINDOW} from '../constants'; - -export default class MenuItem extends PureComponent { - static propTypes = { - icon: PropTypes.string, - label: PropTypes.string.isRequired, - uri: PropTypes.string, - target: PropTypes.string, - isActive: PropTypes.bool.isRequired, - skipI18n: PropTypes.bool, - - onClick: PropTypes.func.isRequired - }; - - handleClick = () => { - const {uri, target, onClick} = this.props; - - onClick(target, uri); - } - - render() { - const {skipI18n, label, icon, uri, target} = this.props; - - const button = ( - - ); - - if (target === TARGET_WINDOW) { - return {button}; - } - return button; - } -} diff --git a/packages/neos-ui/src/Containers/Drawer/MenuItem/index.ts b/packages/neos-ui/src/Containers/Drawer/MenuItem/index.ts new file mode 100644 index 0000000000..ae67147647 --- /dev/null +++ b/packages/neos-ui/src/Containers/Drawer/MenuItem/index.ts @@ -0,0 +1,10 @@ +/* + * This file is part of the Neos.Neos.Ui package. + * + * (c) Contributors of the Neos Project - www.neos.io + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ +export {MenuItem as default} from './MenuItem'; diff --git a/packages/neos-ui/src/Containers/Drawer/MenuItemGroup/MenuItemGroup.tsx b/packages/neos-ui/src/Containers/Drawer/MenuItemGroup/MenuItemGroup.tsx new file mode 100644 index 0000000000..09d97f96d4 --- /dev/null +++ b/packages/neos-ui/src/Containers/Drawer/MenuItemGroup/MenuItemGroup.tsx @@ -0,0 +1,62 @@ +/* + * This file is part of the Neos.Neos.Ui package. + * + * (c) Contributors of the Neos Project - www.neos.io + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ +import React from 'react'; + +import {Icon, ToggablePanel, Button} from '@neos-project/react-ui-components'; + +import I18n from '@neos-project/neos-ui-i18n'; + +import MenuItem from '../MenuItem/index'; +import style from '../style.module.css'; + +export const MenuItemGroup: React.FC<{ + id: string; + icon?: string; + label: string; + uri: string; + target?: string; + collapsed: boolean; + onMenuGroupToggle: (menuGroupId: string) => void; + children: { + icon?: string; + label: string; + uri?: string; + isActive: boolean; + skipI18n?: boolean; + }[]; +}> = (props) => { + const {label, icon, children, uri, collapsed} = props; + const handleMenuGroupToggle = React.useCallback(() => { + props.onMenuGroupToggle(props.id); + }, [props.id]) + + return ( + + + + + + + + {children.map((item, index) => ( + + ))} + + + ); +} diff --git a/packages/neos-ui/src/Containers/Drawer/MenuItemGroup/index.js b/packages/neos-ui/src/Containers/Drawer/MenuItemGroup/index.js deleted file mode 100644 index 5cf82daf35..0000000000 --- a/packages/neos-ui/src/Containers/Drawer/MenuItemGroup/index.js +++ /dev/null @@ -1,73 +0,0 @@ -import React, {PureComponent} from 'react'; -import PropTypes from 'prop-types'; - -import {Icon, ToggablePanel, Button} from '@neos-project/react-ui-components'; - -import I18n from '@neos-project/neos-ui-i18n'; - -import MenuItem from '../MenuItem/index'; -import {TARGET_WINDOW} from '../constants'; -import style from '../style.module.css'; - -export default class MenuItemGroup extends PureComponent { - static propTypes = { - icon: PropTypes.string, - label: PropTypes.string.isRequired, - uri: PropTypes.string.isRequired, - target: PropTypes.string, - collapsed: PropTypes.bool.isRequired, - handleMenuGroupToggle: PropTypes.func.isRequired, - - children: PropTypes.arrayOf( - PropTypes.shape({ - icon: PropTypes.string, - label: PropTypes.string.isRequired, - uri: PropTypes.string, - target: PropTypes.string, - isActive: PropTypes.bool.isRequired, - skipI18n: PropTypes.bool - }) - ), - - onClick: PropTypes.func.isRequired, - onChildClick: PropTypes.func.isRequired - }; - - handleClick = () => { - const {uri, target, onClick} = this.props; - - onClick(target, uri); - } - - render() { - const {label, icon, children, onChildClick, target, uri, collapsed, handleMenuGroupToggle} = this.props; - - const headerButton = ( - - ); - - const header = (target === TARGET_WINDOW ? {headerButton} : headerButton); - - return ( - - - {header} - - - {children.map((item, index) => ( - - ))} - - - ); - } -} diff --git a/packages/neos-ui/src/Containers/Drawer/MenuItemGroup/index.ts b/packages/neos-ui/src/Containers/Drawer/MenuItemGroup/index.ts new file mode 100644 index 0000000000..513ee6aef1 --- /dev/null +++ b/packages/neos-ui/src/Containers/Drawer/MenuItemGroup/index.ts @@ -0,0 +1,10 @@ +/* + * This file is part of the Neos.Neos.Ui package. + * + * (c) Contributors of the Neos Project - www.neos.io + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ +export {MenuItemGroup as default} from './MenuItemGroup'; diff --git a/packages/neos-ui/src/Containers/Drawer/UserDropDown/RestoreButtonItem.js b/packages/neos-ui/src/Containers/Drawer/UserDropDown/RestoreButtonItem.js deleted file mode 100644 index e4fc5840ba..0000000000 --- a/packages/neos-ui/src/Containers/Drawer/UserDropDown/RestoreButtonItem.js +++ /dev/null @@ -1,64 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import {Icon} from '@neos-project/react-ui-components'; -import {neos} from '@neos-project/neos-ui-decorators'; -import {connect} from 'react-redux'; -import {actions} from '@neos-project/neos-ui-redux-store'; -import I18n from '@neos-project/neos-ui-i18n'; - -import buttonTheme from './style.module.css'; - -@connect( - state => ({ - originUser: state?.user?.impersonate?.origin - }), - { - impersonateRestore: actions.User.Impersonate.restore - } -) -@neos(globalRegistry => ({ - i18nRegistry: globalRegistry.get('i18n') -})) -export default class RestoreButtonItem extends React.PureComponent { - static propTypes = { - originUser: PropTypes.object, - impersonateRestore: PropTypes.func.isRequired, - i18nRegistry: PropTypes.object.isRequired - }; - - render() { - const {originUser, i18nRegistry, impersonateRestore} = this.props; - const title = i18nRegistry.translate( - 'impersonate.title.restoreUserButton', - 'Switch back to the orginal user account', - {}, - 'Neos.Neos', - 'Main' - ); - - return (originUser ? ( -
  • - -
  • - ) : null); - } -} diff --git a/packages/neos-ui/src/Containers/Drawer/UserDropDown/RestoreButtonItem.tsx b/packages/neos-ui/src/Containers/Drawer/UserDropDown/RestoreButtonItem.tsx new file mode 100644 index 0000000000..aead35e83b --- /dev/null +++ b/packages/neos-ui/src/Containers/Drawer/UserDropDown/RestoreButtonItem.tsx @@ -0,0 +1,135 @@ +/* + * This file is part of the Neos.Neos.Ui package. + * + * (c) Contributors of the Neos Project - www.neos.io + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ +import React from 'react'; + +import {Icon} from '@neos-project/react-ui-components'; +import I18n from '@neos-project/neos-ui-i18n'; +import {I18nRegistry} from '@neos-project/neos-ts-interfaces'; +import backend from '@neos-project/neos-ui-backend-connector'; + +import {routes} from '../../../System'; + +import buttonTheme from './style.module.css'; + +type ImpersonateAccount = { + accountIdentifier: string; + fullName: string; +}; + +type ImpersonateStatus = { + status: boolean; + user?: ImpersonateAccount; + origin?: ImpersonateAccount; +}; + +export const RestoreButtonItem: React.FC<{ + originUser?: { + fullName: string; + }; + onLoadError: (message: string) => void; + onRestoreSuccess: (message: string) => void; + onRestoreError: (message: string) => void; + i18n: I18nRegistry; +}> = (props) => { + const [impersonateStatus, setImpersonateStatus] = React.useState(null); + const title = props.i18n.translate( + 'impersonate.title.restoreUserButton', + 'Switch back to the orginal user account', + {}, + 'Neos.Neos', + 'Main' + ); + const errorMessage = props.i18n.translate( + 'impersonate.error.restoreUser', + 'Could not switch back to the original user.', + {}, + 'Neos.Neos', + 'Main' + ); + const handleClick = React.useCallback( + async function restoreOriginalUser() { + const {impersonateRestore} = backend.get().endpoints; + const feedback = await impersonateRestore(); + const originUser = feedback?.origin?.accountIdentifier; + const user = feedback?.impersonate?.accountIdentifier; + const status = feedback?.status; + + const restoreMessage = props.i18n.translate( + 'impersonate.success.restoreUser', + 'Switched back from {0} to the orginal user {1}.', + { + 0: user, + 1: originUser + }, + 'Neos.Neos', + 'Main' + ); + + if (status) { + props.onRestoreSuccess(restoreMessage); + } else { + props.onRestoreError(errorMessage); + } + + window.location.href = routes?.core?.modules?.defaultModule; + }, + [props.i18n] + ); + + React.useEffect( + () => { + (async function loadImpersonateStatus(): Promise { + try { + const {impersonateStatus: fetchImpersonateStatus} = + backend.get().endpoints; + const impersonateStatus: null|ImpersonateStatus = + await fetchImpersonateStatus(); + + if (impersonateStatus) { + setImpersonateStatus(impersonateStatus); + } + } catch (error) { + props.onLoadError((error as Error).message); + } + })(); + }, + [] + ); + + if (impersonateStatus?.status !== true) { + return null; + } + + if (!impersonateStatus.origin) { + return null; + } + + return ( +
  • + +
  • + ); +} diff --git a/packages/neos-ui/src/Containers/Drawer/UserDropDown/UserDropDown.tsx b/packages/neos-ui/src/Containers/Drawer/UserDropDown/UserDropDown.tsx new file mode 100644 index 0000000000..e2c5f4c48e --- /dev/null +++ b/packages/neos-ui/src/Containers/Drawer/UserDropDown/UserDropDown.tsx @@ -0,0 +1,89 @@ +/* + * This file is part of the Neos.Neos.Ui package. + * + * (c) Contributors of the Neos Project - www.neos.io + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ +import React from 'react'; + +import {neos} from '@neos-project/neos-ui-decorators'; +import {Icon, DropDown} from '@neos-project/react-ui-components'; +import {I18nRegistry} from '@neos-project/neos-ts-interfaces'; +import {NeosContextInterface} from '@neos-project/neos-ui-decorators/src/neos'; +import {showFlashMessage} from '@neos-project/neos-ui-error'; + +import {UserImage} from './UserImage'; +import {RestoreButtonItem} from './RestoreButtonItem'; + +import I18n from '@neos-project/neos-ui-i18n'; + +import style from './style.module.css'; + +import {user} from '../../../System'; + +const withNeosGlobals = neos(globalRegistry => ({ + i18nRegistry: globalRegistry.get('i18n') +})); + +const UserDropDown: React.FC<{ + i18nRegistry: I18nRegistry; + neos: NeosContextInterface; +}> = (props) => { + const logoutUri = (props.neos?.routes as any)?.core?.logout; + const userSettingsUri = (props.neos?.routes as any)?.core?.modules?.userSettings; + const {csrfToken} = document.getElementById('appContainer')!.dataset; + + return ( +
    + + + + {user.name.fullName} + + +
  • + + +
  • +
  • +
    + + +
    +
  • + showFlashMessage({ + id: 'impersonateStatusError', + severity: 'error', + message + })} + onRestoreSuccess={(message) => showFlashMessage({ + id: 'restoreUserImpersonateUser', + severity: 'success', + message + })} + onRestoreError={(message) => showFlashMessage({ + id: 'restoreUserImpersonateUser', + severity: 'success', + message + })} + /> +
    +
    +
    + ); +} + +export default withNeosGlobals(UserDropDown as any); diff --git a/packages/neos-ui/src/Containers/Drawer/UserDropDown/UserImage.js b/packages/neos-ui/src/Containers/Drawer/UserDropDown/UserImage.js deleted file mode 100644 index 17d2293340..0000000000 --- a/packages/neos-ui/src/Containers/Drawer/UserDropDown/UserImage.js +++ /dev/null @@ -1,24 +0,0 @@ -import React, {PureComponent} from 'react'; -import PropTypes from 'prop-types'; -import {connect} from 'react-redux'; - -import style from './style.module.css'; - -@connect(state => ({ - userFirstName: state?.user?.name?.firstName, - userLastName: state?.user?.name?.lastName -})) - -export default class UserImage extends PureComponent { - static propTypes = { - userFirstName: PropTypes.string, - userLastName: PropTypes.string - }; - - render() { - const userInitials = this.props.userFirstName?.charAt(0) + this.props.userLastName?.charAt(0); - return ( -
    {userInitials}
    - ); - } -} diff --git a/packages/neos-ui/src/Containers/Drawer/UserDropDown/UserImage.tsx b/packages/neos-ui/src/Containers/Drawer/UserDropDown/UserImage.tsx new file mode 100644 index 0000000000..b432409ba0 --- /dev/null +++ b/packages/neos-ui/src/Containers/Drawer/UserDropDown/UserImage.tsx @@ -0,0 +1,23 @@ +/* + * This file is part of the Neos.Neos.Ui package. + * + * (c) Contributors of the Neos Project - www.neos.io + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ +import React from 'react'; + +import style from './style.module.css'; + +export const UserImage: React.FC<{ + userFirstName: string; + userLastName: string; +}> = (props) => { + const userInitials = props.userFirstName?.charAt(0) + props.userLastName?.charAt(0); + + return ( +
    {userInitials}
    + ); +} diff --git a/packages/neos-ui/src/Containers/Drawer/UserDropDown/index.js b/packages/neos-ui/src/Containers/Drawer/UserDropDown/index.js deleted file mode 100644 index 18efa136ab..0000000000 --- a/packages/neos-ui/src/Containers/Drawer/UserDropDown/index.js +++ /dev/null @@ -1,59 +0,0 @@ -import React, {PureComponent} from 'react'; -import PropTypes from 'prop-types'; -import {connect} from 'react-redux'; - -import {neos} from '@neos-project/neos-ui-decorators'; -import {Icon, DropDown} from '@neos-project/react-ui-components'; -import UserImage from './UserImage'; -import RestoreButtonItem from './RestoreButtonItem'; - -import I18n from '@neos-project/neos-ui-i18n'; - -import style from './style.module.css'; -@connect(state => ({ - userName: state?.user?.name?.fullName, - impersonateStatus: state?.user?.impersonate?.status -})) -@neos() -export default class UserDropDown extends PureComponent { - static propTypes = { - userName: PropTypes.string.isRequired, - impersonateStatus: PropTypes.bool.isRequired - }; - - render() { - const logoutUri = this.props.neos?.routes?.core?.logout; - const userSettingsUri = this.props.neos?.routes?.core?.modules?.userSettings; - const {csrfToken} = document.getElementById('appContainer').dataset; - return ( -
    - - - - {this.props.userName} - - -
  • - - -
  • -
  • -
    - - -
    -
  • - {this.props.impersonateStatus === true ? ( - - ) : null} -
    -
    -
    - ); - } -} diff --git a/packages/neos-ui/src/Containers/Drawer/UserDropDown/index.ts b/packages/neos-ui/src/Containers/Drawer/UserDropDown/index.ts new file mode 100644 index 0000000000..63a4888e9f --- /dev/null +++ b/packages/neos-ui/src/Containers/Drawer/UserDropDown/index.ts @@ -0,0 +1,10 @@ +/* + * This file is part of the Neos.Neos.Ui package. + * + * (c) Contributors of the Neos Project - www.neos.io + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ +export {default} from './UserDropDown'; diff --git a/packages/neos-ui/src/Containers/Drawer/VersionPanel/VersionPanel.tsx b/packages/neos-ui/src/Containers/Drawer/VersionPanel/VersionPanel.tsx new file mode 100644 index 0000000000..87d13bfe04 --- /dev/null +++ b/packages/neos-ui/src/Containers/Drawer/VersionPanel/VersionPanel.tsx @@ -0,0 +1,20 @@ +/* + * This file is part of the Neos.Neos.Ui package. + * + * (c) Contributors of the Neos Project - www.neos.io + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ +import React from 'react'; +import {getVersion} from '@neos-project/utils-helpers'; + +import style from '../style.module.css'; + +const uiVersion = getVersion(); +export const VersionPanel: React.FC = () => ( +
    +
    UI version: {uiVersion}
    +
    +); diff --git a/packages/neos-ui/src/Containers/Drawer/VersionPanel/index.js b/packages/neos-ui/src/Containers/Drawer/VersionPanel/index.js deleted file mode 100644 index 8eecf1d727..0000000000 --- a/packages/neos-ui/src/Containers/Drawer/VersionPanel/index.js +++ /dev/null @@ -1,17 +0,0 @@ -import React, {PureComponent} from 'react'; -import {getVersion} from '@neos-project/utils-helpers'; - -import style from '../style.module.css'; - -export default class VersionPanel extends PureComponent { - render() { - // Current version to enhance bugreports - const uiVersion = getVersion(); - - return ( -
    -
    UI version: {uiVersion}
    -
    - ); - } -} diff --git a/packages/neos-ui/src/Containers/Drawer/VersionPanel/index.ts b/packages/neos-ui/src/Containers/Drawer/VersionPanel/index.ts new file mode 100644 index 0000000000..f77ad3865a --- /dev/null +++ b/packages/neos-ui/src/Containers/Drawer/VersionPanel/index.ts @@ -0,0 +1,10 @@ +/* + * This file is part of the Neos.Neos.Ui package. + * + * (c) Contributors of the Neos Project - www.neos.io + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ +export {VersionPanel as default} from './VersionPanel'; diff --git a/packages/neos-ui/src/Containers/Drawer/constants.js b/packages/neos-ui/src/Containers/Drawer/constants.js deleted file mode 100644 index 15f065a3d8..0000000000 --- a/packages/neos-ui/src/Containers/Drawer/constants.js +++ /dev/null @@ -1,3 +0,0 @@ -export const TARGET_WINDOW = 'Window'; -export const TARGET_CONTENT_CANVAS = 'ContentCanvas'; -export const THRESHOLD_MOUSE_LEAVE = 500; diff --git a/packages/neos-ui/src/Containers/Drawer/constants.ts b/packages/neos-ui/src/Containers/Drawer/constants.ts new file mode 100644 index 0000000000..bdbad066c0 --- /dev/null +++ b/packages/neos-ui/src/Containers/Drawer/constants.ts @@ -0,0 +1,10 @@ +/* + * This file is part of the Neos.Neos.Ui package. + * + * (c) Contributors of the Neos Project - www.neos.io + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ +export const THRESHOLD_MOUSE_LEAVE = 500; diff --git a/packages/neos-ui/src/Containers/Drawer/index.js b/packages/neos-ui/src/Containers/Drawer/index.js deleted file mode 100644 index a73f45aabc..0000000000 --- a/packages/neos-ui/src/Containers/Drawer/index.js +++ /dev/null @@ -1,141 +0,0 @@ -import React, {PureComponent} from 'react'; -import PropTypes from 'prop-types'; -import mergeClassNames from 'classnames'; -import {connect} from 'react-redux'; - -import {actions} from '@neos-project/neos-ui-redux-store'; -import {neos} from '@neos-project/neos-ui-decorators'; - -import MenuItemGroup from './MenuItemGroup/index'; -import style from './style.module.css'; -import {TARGET_WINDOW, TARGET_CONTENT_CANVAS, THRESHOLD_MOUSE_LEAVE} from './constants'; - -@neos(globalRegistry => ({ - containerRegistry: globalRegistry.get('containers') -})) -@connect(state => ({ - isHidden: state?.ui?.drawer?.isHidden, - collapsedMenuGroups: state?.ui?.drawer?.collapsedMenuGroups -}), { - hideDrawer: actions.UI.Drawer.hide, - toggleMenuGroup: actions.UI.Drawer.toggleMenuGroup, - setContentCanvasSrc: actions.UI.ContentCanvas.setSrc -}) -export default class Drawer extends PureComponent { - static propTypes = { - isHidden: PropTypes.bool.isRequired, - collapsedMenuGroups: PropTypes.array.isRequired, - - hideDrawer: PropTypes.func.isRequired, - toggleMenuGroup: PropTypes.func.isRequired, - setContentCanvasSrc: PropTypes.func.isRequired, - - containerRegistry: PropTypes.object.isRequired, - - menuData: PropTypes.arrayOf( - PropTypes.shape({ - icon: PropTypes.string, - label: PropTypes.string.isRequired, - uri: PropTypes.string.isRequired, - target: PropTypes.string, - - children: PropTypes.arrayOf( - PropTypes.shape({ - icon: PropTypes.string, - label: PropTypes.string.isRequired, - uri: PropTypes.string, - target: PropTypes.string, - isActive: PropTypes.bool.isRequired, - skipI18n: PropTypes.bool.isRequired - }) - ) - }) - ).isRequired - }; - - state = { - mouseLeaveTimeout: null - }; - - handleMouseLeave = () => { - const {hideDrawer} = this.props; - const {mouseLeaveTimeout} = this.state; - - if (!mouseLeaveTimeout) { - const timeout = setTimeout(() => { - hideDrawer(); - this.setState({ - mouseLeaveTimeout: null - }); - }, THRESHOLD_MOUSE_LEAVE); - - this.setState({ - mouseLeaveTimeout: timeout - }); - } - } - - handleMouseEnter = () => { - const {mouseLeaveTimeout} = this.state; - - if (mouseLeaveTimeout) { - clearTimeout(mouseLeaveTimeout); - this.setState({ - mouseLeaveTimeout: null - }); - } - } - - handleMenuItemClick = (target, uri) => { - const {setContentCanvasSrc, hideDrawer} = this.props; - - switch (target) { - case TARGET_CONTENT_CANVAS: - setContentCanvasSrc(uri); - hideDrawer(); - break; - - case TARGET_WINDOW: - default: - // we do not need to do anything here, as MenuItems of type TARGET_WINDOW automatically - // wrap their contents in an -tag (such that the user can crtl-click it to open in a - // new window). - break; - } - } - - render() { - const {isHidden, menuData, collapsedMenuGroups, toggleMenuGroup, containerRegistry} = this.props; - const classNames = mergeClassNames({ - [style.drawer]: true, - [style['drawer--isHidden']]: isHidden - }); - - const BottomComponents = containerRegistry.getChildren('Drawer/Bottom'); - - return ( -
    -
    - {!isHidden && Object.entries(menuData).map(([menuGroup, menuGroupConfiguration]) => ( - toggleMenuGroup(menuGroup)} - {...menuGroupConfiguration} - /> - ))} -
    -
    - {BottomComponents.map((Item, key) => )} -
    -
    - ); - } -} diff --git a/packages/neos-ui/src/Containers/Drawer/index.ts b/packages/neos-ui/src/Containers/Drawer/index.ts new file mode 100644 index 0000000000..122c4c1382 --- /dev/null +++ b/packages/neos-ui/src/Containers/Drawer/index.ts @@ -0,0 +1,10 @@ +/* + * This file is part of the Neos.Neos.Ui package. + * + * (c) Contributors of the Neos Project - www.neos.io + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ +export {Drawer as default, drawer$, toggleDrawer} from './Drawer'; diff --git a/packages/neos-ui/src/Containers/PrimaryToolbar/MenuToggler/MenuToggler.tsx b/packages/neos-ui/src/Containers/PrimaryToolbar/MenuToggler/MenuToggler.tsx new file mode 100644 index 0000000000..96b7cce780 --- /dev/null +++ b/packages/neos-ui/src/Containers/PrimaryToolbar/MenuToggler/MenuToggler.tsx @@ -0,0 +1,65 @@ +/* + * This file is part of the Neos.Neos.Ui package. + * + * (c) Contributors of the Neos Project - www.neos.io + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ +import React from 'react'; +import mergeClassNames from 'classnames'; + +import Button from '@neos-project/react-ui-components/src/Button/'; +import {neos} from '@neos-project/neos-ui-decorators'; +import {I18nRegistry} from '@neos-project/neos-ts-interfaces'; +import {useLatestState} from '@neos-project/framework-observable-react'; + +import {drawer$, toggleDrawer} from '../../Drawer'; + +import style from './style.module.css'; + +const withNeosGlobals = neos(globalRegistry => ({ + i18nRegistry: globalRegistry.get('i18n') +})); + +const StatelessMenuToggler: React.FC<{ + i18nRegistry: I18nRegistry; + + className?: string; +}> = (props) => { + const handleToggle = React.useCallback(() => { + toggleDrawer(); + }, []); + + const {className, i18nRegistry} = props; + const {isHidden: isMenuHidden} = useLatestState(drawer$); + const isMenuVisible = !isMenuHidden; + const classNames = mergeClassNames({ + [style.menuToggler]: true, + [style['menuToggler--isActive']]: isMenuVisible, + [className ?? '']: className && className.length + }); + + // + // ToDo: Replace the static 'Menu' aria-label with a label from the i18n service. + // + return ( + + ); +} + +export const MenuToggler = withNeosGlobals(StatelessMenuToggler as any); diff --git a/packages/neos-ui/src/Containers/PrimaryToolbar/MenuToggler/index.js b/packages/neos-ui/src/Containers/PrimaryToolbar/MenuToggler/index.js deleted file mode 100644 index 527fb3de97..0000000000 --- a/packages/neos-ui/src/Containers/PrimaryToolbar/MenuToggler/index.js +++ /dev/null @@ -1,65 +0,0 @@ -import React, {PureComponent} from 'react'; -import PropTypes from 'prop-types'; -import {connect} from 'react-redux'; -import mergeClassNames from 'classnames'; - -import Button from '@neos-project/react-ui-components/src/Button/'; -import {actions} from '@neos-project/neos-ui-redux-store'; -import {neos} from '@neos-project/neos-ui-decorators'; - -import style from './style.module.css'; - -@neos(globalRegistry => ({ - i18nRegistry: globalRegistry.get('i18n') -})) - -@connect(state => ({ - isMenuHidden: state?.ui?.drawer?.isHidden -}), { - toggleDrawer: actions.UI.Drawer.toggle -}) -export default class MenuToggler extends PureComponent { - static propTypes = { - i18nRegistry: PropTypes.object.isRequired, - - className: PropTypes.string, - isMenuHidden: PropTypes.bool.isRequired, - toggleDrawer: PropTypes.func.isRequired - }; - - handleToggle = () => { - const {toggleDrawer} = this.props; - - toggleDrawer(); - } - - render() { - const {className, isMenuHidden, i18nRegistry} = this.props; - const isMenuVisible = !isMenuHidden; - const classNames = mergeClassNames({ - [style.menuToggler]: true, - [style['menuToggler--isActive']]: isMenuVisible, - [className]: className && className.length - }); - - // - // ToDo: Replace the static 'Menu' aria-label with a label from the i18n service. - // - return ( - - ); - } -} diff --git a/packages/neos-ui/src/Containers/PrimaryToolbar/MenuToggler/index.ts b/packages/neos-ui/src/Containers/PrimaryToolbar/MenuToggler/index.ts new file mode 100644 index 0000000000..945d17c0ef --- /dev/null +++ b/packages/neos-ui/src/Containers/PrimaryToolbar/MenuToggler/index.ts @@ -0,0 +1,10 @@ +/* + * This file is part of the Neos.Neos.Ui package. + * + * (c) Contributors of the Neos Project - www.neos.io + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ +export {MenuToggler as default} from './MenuToggler'; diff --git a/packages/neos-ui/src/System/index.js b/packages/neos-ui/src/System/index.ts similarity index 65% rename from packages/neos-ui/src/System/index.js rename to packages/neos-ui/src/System/index.ts index d1ae9857e5..e3ae611a51 100644 --- a/packages/neos-ui/src/System/index.js +++ b/packages/neos-ui/src/System/index.ts @@ -1,8 +1,17 @@ +/* + * This file is part of the Neos.Neos.Ui package. + * + * (c) Contributors of the Neos Project - www.neos.io + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ import {initializeJsAPI} from '@neos-project/neos-ui-backend-connector'; import fetchWithErrorHandling from '@neos-project/neos-ui-backend-connector/src/FetchWithErrorHandling/index'; import {terminateDueToFatalInitializationError} from '@neos-project/neos-ui-error'; -let initialData = null; +let initialData: any = null; function parseInitialData() { if (initialData) { return initialData; @@ -36,7 +45,7 @@ function parseInitialData() { } } -function getInlinedData(dataName) { +function getInlinedData(dataName: string): any { const initialData = parseInitialData(); if (dataName in initialData) { @@ -49,30 +58,32 @@ function getInlinedData(dataName) { `); } -export const appContainer = document.getElementById('appContainer'); -if (!appContainer) { +const appContainerOrNull = document.getElementById('appContainer'); +if (!appContainerOrNull) { terminateDueToFatalInitializationError(`

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

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

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

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

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

    `); } +export const systemEnv = systemEnvOrNull; export const serverState = getInlinedData('initialState'); @@ -84,7 +95,34 @@ export const frontendConfiguration = getInlinedData('frontendConfiguration'); export const routes = getInlinedData('routes'); -export const menu = getInlinedData('menu'); +export const menu: { + label: string; + icon: string; + uri: string; + target: 'Window'; + children: { + icon: string; + label: string; + uri: string; + target: 'Window'; + isActive: boolean; + skipI18n: boolean; + }[] +}[] = getInlinedData('menu'); + +export const user: { + name: { + title: string; + firstName: string; + middleName: string; + lastName: string; + otherName: string; + fullName: string; + }; + preferences: { + interfaceLanguage: null | string; + }; +} = getInlinedData('user'); export const neos = initializeJsAPI(window, { systemEnv, diff --git a/packages/neos-ui/src/index.js b/packages/neos-ui/src/index.js index e1b7a97c10..55845a1b30 100644 --- a/packages/neos-ui/src/index.js +++ b/packages/neos-ui/src/index.js @@ -20,6 +20,7 @@ import { routes, serverState, menu, + user, nodeTypes } from './System'; import localStorageMiddleware from './localStorageMiddleware'; @@ -65,8 +66,7 @@ async function main() { await Promise.all([ loadNodeTypesSchema(), - initializeI18n(), - loadImpersonateStatus() + initializeI18n() ]); store.dispatch(actions.System.ready()); @@ -104,7 +104,17 @@ function initializeReduxState() { const persistedState = localStorage.getItem('persistedState') ? JSON.parse(localStorage.getItem('persistedState')) : {}; - const mergedState = merge({}, serverState, persistedState); + const mergedState = merge( + {}, + serverState, + // QUIRK ALERT: + // The `user` state used to be part of `initialState` (a.k.a. + // `serverState`) but has been moved to a separate key within + // `initialData`. It is still being merged at this point to + // keep downstream impact at a minimum. + {user}, + persistedState + ); store.dispatch(actions.System.init(mergedState)); } @@ -171,22 +181,6 @@ async function loadNodeTypesSchema() { nodeTypesRegistry.setRoles(roles); } -async function loadImpersonateStatus() { - try { - const {impersonateStatus} = backend.get().endpoints; - const impersonateState = await impersonateStatus(); - if (impersonateState) { - store.dispatch(actions.User.Impersonate.fetchStatus(impersonateState)); - } - } catch (error) { - showFlashMessage({ - id: 'impersonateStatusError', - severity: 'error', - message: error.message - }); - } -} - function renderApplication() { ReactDOM.render( should not render when having no children. 1`] = ` + +`; diff --git a/packages/react-ui-components/src/DropDown/contents.spec.tsx b/packages/react-ui-components/src/DropDown/contents.spec.tsx index 205d9914ee..85bd3fb1a9 100644 --- a/packages/react-ui-components/src/DropDown/contents.spec.tsx +++ b/packages/react-ui-components/src/DropDown/contents.spec.tsx @@ -21,7 +21,7 @@ describe('', () => { it('should not render when having no children.', () => { const wrapper = shallow(); - expect(toJson(wrapper)).toBeFalsy(); + expect(toJson(wrapper)).toMatchSnapshot(); }); it('should allow the propagation of "className" with the "className" prop.', () => { diff --git a/packages/react-ui-components/src/DropDown/contents.tsx b/packages/react-ui-components/src/DropDown/contents.tsx index 854a525d38..ce2d17b943 100644 --- a/packages/react-ui-components/src/DropDown/contents.tsx +++ b/packages/react-ui-components/src/DropDown/contents.tsx @@ -220,26 +220,23 @@ export default class ShallowDropDownContents extends PureComponent - {children} - - ); - - return scrollable - ? ReactDOM.createPortal(contents, document.body) - : contents; - } - return null; + const contents = ( +
      + {children} +
    + ); + + return scrollable + ? ReactDOM.createPortal(contents, document.body) + : contents; } } diff --git a/packages/react-ui-components/src/DropDown/index.ts b/packages/react-ui-components/src/DropDown/index.ts index 92f50750f8..477d3d8712 100644 --- a/packages/react-ui-components/src/DropDown/index.ts +++ b/packages/react-ui-components/src/DropDown/index.ts @@ -1,3 +1,12 @@ +/* + * This file is part of the Neos.Neos.Ui package. + * + * (c) Contributors of the Neos Project - www.neos.io + * + * This package is Open Source Software. For the full copyright and license + * information, please view the LICENSE file which was distributed with this + * source code. + */ import {themr} from '@friendsofreactjs/react-css-themr'; import identifiers from '../identifiers'; @@ -14,11 +23,8 @@ const StatelessDropDown = themr(identifiers.dropDown, style)(StatelessDropDownWr const DropDownHeader = themr(identifiers.dropDownHeader, style)(ContextDropDownHeader); const DropDownContents = themr(identifiers.dropDownContents, style)(ContextDropDownContents); -// @ts-ignore -DropDown.Header = DropDownHeader; -// @ts-ignore -DropDown.Contents = DropDownContents; -// @ts-ignore -DropDown.Stateless = StatelessDropDown; - -export default DropDown; +export default Object.assign(DropDown, { + Header: DropDownHeader, + Contents: DropDownContents, + Stateless: StatelessDropDown +}); diff --git a/yarn.lock b/yarn.lock index a1d3d591f1..da9004b1e5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3770,6 +3770,8 @@ __metadata: "@fortawesome/free-solid-svg-icons": ^6.5.2 "@friendsofreactjs/react-css-themr": ~4.2.0 "@neos-project/debug-reason-for-rendering": "workspace:*" + "@neos-project/framework-observable": "workspace:*" + "@neos-project/framework-observable-react": "workspace:*" "@neos-project/jest-preset-neos-ui": "workspace:*" "@neos-project/neos-ts-interfaces": "workspace:*" "@neos-project/neos-ui-backend-connector": "workspace:*"