Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Isolate Drawer menu from the rest of the UI #3773

Draft
wants to merge 22 commits into
base: 9.0
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
c573752
!!!TASK: Split `user` key from `initialData` via separate `UserProvider`
grebaldi Apr 30, 2024
6b2ab97
TASK: Convert `<UserImage/>` container to typescript
grebaldi Apr 30, 2024
9bd2d84
TASK: Convert `<RestoreButtonItem/>` container to typescript
grebaldi Apr 30, 2024
82c2bb2
TASK: Convert `<UserDropDown/>` container to typescript
grebaldi Apr 30, 2024
f91d89a
PATCH: Fix typing in UserProviderInterface
grebaldi May 21, 2024
9aa6548
TASK: Convert parsing of initial data to Typescript
grebaldi May 21, 2024
6b8b823
TASK: Make `alias`optional in `initializeJsAPI`
grebaldi May 21, 2024
f65f3cb
TASK: Detach user menu from redux store
grebaldi May 21, 2024
428eb0a
BUGFIX: Render dropdown contents even when dropdown is closed
grebaldi May 30, 2024
0d1d8d4
TASK: Convert VersionPanel component to typescript
grebaldi May 30, 2024
69056eb
TASK: Convert Drawer/constants to typescript
grebaldi May 30, 2024
0a7ee42
TASK: Convert Drawer/MenuItem component to typescript
grebaldi May 30, 2024
9f2cd25
TASK: Convert Drawer/MenuItemGroup to typescript
grebaldi May 30, 2024
c230da3
TASK: Convert Drawer component to typescript
grebaldi May 30, 2024
5328db4
PATCH: Don't pass user down to Drawer & UserDropDown
grebaldi May 30, 2024
f38ef47
PATCH: Move impersonateRestore out of the redux store
grebaldi May 30, 2024
f713159
PATCH: Move impersonateRestore out of the redux store part II
grebaldi Jun 13, 2024
50bedad
TASK: Remove unused `target` property from drawer menu items
grebaldi Jun 13, 2024
75ecb7a
TASK: Convert MenuToggler to typescript
grebaldi Jun 14, 2024
7e04b2a
TASK: Decouple Drawer from redux store
grebaldi Jun 14, 2024
1023f8e
TASK: Followup c6b138396593cee9bff5f077784558ee33cbdde3
mhsdesign Jan 14, 2025
d1f2456
TASK: Make linting happy for now
mhsdesign Jan 14, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 22 additions & 9 deletions Classes/Controller/BackendController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -45,12 +46,6 @@ class BackendController extends ActionController

protected $defaultViewObjectName = ApplicationView::class;

/**
* @Flow\Inject
* @var UserService
*/
protected $userService;

/**
* @Flow\Inject
* @var DomainRepository
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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'));
Expand Down Expand Up @@ -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' =>
Expand All @@ -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,
),
]);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,5 @@ public function getInitialState(
ActionRequest $actionRequest,
?Node $documentNode,
?Node $site,
User $user,
): array;
}
2 changes: 1 addition & 1 deletion Classes/Domain/InitialData/MenuProviderInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
interface MenuProviderInterface
{
/**
* @return array<int,array{label:string,icon:string,uri:string,target:string,children:array<int,array{icon:string,label:string,uri:string,position?:string,isActive:bool,target:string,skipI18n:bool}>}>
* @return array<int,array{label:string,icon:string,uri:string,children:array<int,array{icon:string,label:string,uri:string,position?:string,isActive:bool,skipI18n:bool}>}>
*/
public function getMenu(ActionRequest $actionRequest): array;
}
29 changes: 29 additions & 0 deletions Classes/Domain/InitialData/UserProviderInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

/*
* 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.
*/

declare(strict_types=1);

namespace Neos\Neos\Ui\Domain\InitialData;

/**
* Retrieves all data needed to render the user panel at the bottom
* of the drawer menu
*
* @internal
*/
interface UserProviderInterface
{
/**
* @return array{name:array{title:string,firstName:string,middleName:string,lastName:string,otherName:string,fullName:string},preferences:array{interfaceLanguage:null|string}}
*/
public function getUser(): ?array;
}
2 changes: 0 additions & 2 deletions Classes/Infrastructure/Configuration/InitialStateProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,13 @@ public function getInitialState(
ActionRequest $actionRequest,
?Node $documentNode,
?Node $site,
User $user,
): array {
return $this->configurationRenderingService->computeConfiguration(
$this->initialStateBeforeProcessing,
[
'request' => $actionRequest,
'documentNode' => $documentNode,
'site' => $site,
'user' => $user,
'clipboardNodes' => $this->clipboard->getSerializedNodeAddresses(),
'clipboardMode' => $this->clipboard->getMode(),
]
Expand Down
5 changes: 1 addition & 4 deletions Classes/Infrastructure/Neos/MenuProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -67,7 +66,7 @@ public function getMenu(ActionRequest $actionRequest): array
}

/**
* @return array<int,array{icon:string,label:string,uri:string,isActive:bool,target:string,skipI18n:bool}>
* @return array<int,array{icon:string,label:string,uri:string,isActive:bool,skipI18n:bool}>
*/
private function buildChildrenForSites(ControllerContext $controllerContext): array
{
Expand All @@ -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;
}
Expand All @@ -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;
}

Expand Down
55 changes: 55 additions & 0 deletions Classes/Infrastructure/Neos/UserProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

/*
* 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.
*/

declare(strict_types=1);

namespace Neos\Neos\Ui\Infrastructure\Neos;

use Neos\Flow\Annotations as Flow;
use Neos\Neos\Service\UserService;
use Neos\Neos\Ui\Domain\InitialData\UserProviderInterface;

/**
* @internal
*/
#[Flow\Scope("singleton")]
final class UserProvider implements UserProviderInterface
{
#[Flow\Inject]
protected UserService $userService;

/**
* @return array{name:array{title:string,firstName:string,middleName:string,lastName:string,otherName:string,fullName:string},preferences:array{interfaceLanguage:null|string}}
*/
public function getUser(): ?array
{
$user = $this->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(),
],
];
}
}
3 changes: 3 additions & 0 deletions Configuration/Objects.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion packages/neos-ui-backend-connector/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.`);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
Expand Down
2 changes: 2 additions & 0 deletions packages/neos-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:*",
Expand Down
121 changes: 121 additions & 0 deletions packages/neos-ui/src/Containers/Drawer/Drawer.tsx
Original file line number Diff line number Diff line change
@@ -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<any>;

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 | ReturnType<typeof setTimeout>>(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 (
<div
className={classNames}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
aria-hidden={isHidden ? 'true' : 'false'}
>
<div className={style.drawer__menuItemGroupsWrapper}>
{Object.entries(menuData).map(([menuGroup, menuGroupConfiguration]) => (
<MenuItemGroup
key={menuGroup}
id={menuGroup}
collapsed={collapsedMenuGroups.includes(menuGroup)}
onMenuGroupToggle={toggleMenuGroup}
{...menuGroupConfiguration}
/>
))}
</div>
<div className={style.drawer__bottom}>
{BottomComponents.map((Item, key) => <Item key={key}/>)}
</div>
</div>
);
}

export const Drawer = withNeosGlobals(StatelessDrawer as any);
Loading
Loading