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 71 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
71 commits
Select commit Hold shift + click to select a range
a9b33d1
TASK: Convert FlashMessage component to typescript
grebaldi May 30, 2024
10068f1
TASK: Convert FlashMessages component to typescript
grebaldi May 31, 2024
28f91a3
TASK: Move ErrorView component into ./container/ directory
grebaldi May 31, 2024
57af79b
TASK: Move ErrorBoundary component to neos-ui-error package
grebaldi May 31, 2024
492f0cd
TASK: Move function `terminateDueToFatalInitializationError` to neos-…
grebaldi May 31, 2024
7cd6b2d
TASK: Move component FlashMessages to neos-ui-error package
grebaldi May 31, 2024
2538b85
TASK: Add package `@neos-project/framework-observable`
grebaldi May 31, 2024
3a9090e
TASK: Add package `@neos-project/framework-observable-react`
grebaldi May 31, 2024
ff458b6
TASK: Decouple FlashMessage from redux store
grebaldi May 31, 2024
2258a0b
!!!TASK: Delete obsolete redux-store elements for flash messages
grebaldi Jun 6, 2024
2caf38a
TASK: Replace all calls to `action.UI.FlashMessages.add` with new `sh…
grebaldi Jun 6, 2024
8823c4f
TASK: Implement `TranslationAddress` value object
grebaldi Jun 14, 2024
1c714e9
TASK: Extract function `getTranslationAddress`
grebaldi Jun 14, 2024
9cf40bd
TASK: Extract function `substitutePlaceholders`
grebaldi Jun 14, 2024
76614b5
TASK: Extract function `getPluralForm`
grebaldi Jun 17, 2024
c5f98b4
TASK: Convert I18nRegistry to typescript
grebaldi Jun 17, 2024
c3d5300
TASK: Return actual TranslationAddress from `getTranslationAddress`
grebaldi Jun 17, 2024
fa66f2a
TASK: Introduce TranslationUnitRepository
grebaldi Jun 17, 2024
d35af00
TASK: Preserve fully qualified translation address
grebaldi Jun 17, 2024
f579a54
TASK: Fix misnomer f.q. trans-unit id -> f.q. translation address
grebaldi Jun 17, 2024
99179ad
TASK: Use TranslationUnitRepository for translation lookup in I18nReg…
grebaldi Jun 17, 2024
0f6fde3
TASK: Rename TranslationUnit -> TranslationUnitDTO
grebaldi Jun 17, 2024
6b839e0
TASK: Introduce TranslationUnit value object
grebaldi Jun 17, 2024
eac4a03
TASK: Integrate TranslationUnit with I18nRegistry
grebaldi Jun 17, 2024
fc046ea
TASK: Remove obsolete function `getPluralForm`
grebaldi Jun 17, 2024
ed34e6a
TASK: Convert neos-ui-i18n/src/registry/I18nRegistry.spec.js to TypeS…
grebaldi Jun 17, 2024
339684d
TASK: Turn `I18nRegistry` into a singleton
grebaldi Jun 17, 2024
3c94d1e
TASK: Make <I18n> component independent from global registry
grebaldi Jun 17, 2024
b60daf3
TASK: Expose I18nRegistry through i18n package rather than neos-ts-in…
grebaldi Jun 17, 2024
2d6e743
TASK: Convert neos-ui-i18n/index.spec.js to TypeScript
grebaldi Jun 17, 2024
7a06dea
TASK: Rename `TranslationUnit` -> `Translation`
grebaldi Jun 18, 2024
5ecfb4f
TASK: Expose function `registerTranslations` from @neos-project/neos-…
grebaldi Jun 18, 2024
6da83b4
TASK: Discover translations endpoint via <link>-tag
grebaldi Jun 19, 2024
9375354
TASK: Introduce Locale & PluralRule models
grebaldi Jun 25, 2024
e6716fb
TASK: Expose `registerLocale` and use it in bootstrap process
grebaldi Jun 25, 2024
61168df
!!!BUGFIX: Use Intl plurals for proper plural form determination
grebaldi Jun 25, 2024
497a345
FEATURE: Implement and expose API `translate` function
grebaldi Jun 26, 2024
77dfdda
TASK: Centralize setup of globals for @neos-project/neos-ui-i18n
grebaldi Jun 26, 2024
f5eb665
TASK: Implement, expose and use `initializeI18n`
grebaldi Jun 27, 2024
5278b3e
TASK: Move `Translation` into `model` module
grebaldi Jun 27, 2024
047a39f
TASK: Move `TranslationAddress` into `model` module
grebaldi Jun 27, 2024
16f3efb
TASK: Move `TranslationRepository` into `model` module
grebaldi Jun 27, 2024
0431bdf
TASK: Rename `Parameters` -> `LegacyParameters`
grebaldi Jun 27, 2024
1ac7864
TASK: Introduce proper `Parameters` type
grebaldi Jun 27, 2024
c567182
TASK: Move `<I18n/>`-component into separate module
grebaldi Jun 28, 2024
a875f9a
TASK: Deprecate `I18nRegistry.translate`
grebaldi Jun 28, 2024
6c2fd85
TASK: Deprecate `I18nRegistry`
grebaldi Jun 28, 2024
9f46e0a
TASK: Deprecate `<I18n/>`-component
grebaldi Jun 28, 2024
232e6a8
TASK: Add documentation for `@neos-project/neos-ui-i18n` package
grebaldi Jun 28, 2024
4131c8a
Merge remote-tracking branch 'upstream/task/centralize-error-handling…
grebaldi Jul 5, 2024
15c94f3
Merge remote-tracking branch 'upstream/task/centralize-i18n' into tem…
grebaldi Jul 5, 2024
26cebfe
!!!TASK: Split `user` key from `initialData` via separate `UserProvider`
grebaldi Apr 30, 2024
1d5f669
TASK: Convert `<UserImage/>` container to typescript
grebaldi Apr 30, 2024
916059f
TASK: Convert `<RestoreButtonItem/>` container to typescript
grebaldi Apr 30, 2024
437fa07
TASK: Convert `<UserDropDown/>` container to typescript
grebaldi Apr 30, 2024
0d83a02
PATCH: Fix typing in UserProviderInterface
grebaldi May 21, 2024
91a8f8b
TASK: Convert parsing of initial data to Typescript
grebaldi May 21, 2024
23511f4
TASK: Make `alias`optional in `initializeJsAPI`
grebaldi May 21, 2024
5691af5
TASK: Detach user menu from redux store
grebaldi May 21, 2024
f440199
BUGFIX: Render dropdown contents even when dropdown is closed
grebaldi May 30, 2024
7393f33
TASK: Convert VersionPanel component to typescript
grebaldi May 30, 2024
0da253e
TASK: Convert Drawer/constants to typescript
grebaldi May 30, 2024
393902b
TASK: Convert Drawer/MenuItem component to typescript
grebaldi May 30, 2024
46e8ba0
TASK: Convert Drawer/MenuItemGroup to typescript
grebaldi May 30, 2024
22dcadf
TASK: Convert Drawer component to typescript
grebaldi May 30, 2024
1ef89fd
PATCH: Don't pass user down to Drawer & UserDropDown
grebaldi May 30, 2024
87300fa
PATCH: Move impersonateRestore out of the redux store
grebaldi May 30, 2024
c4a3d71
PATCH: Move impersonateRestore out of the redux store part II
grebaldi Jun 13, 2024
c6b1383
TASK: Remove unused `target` property from drawer menu items
grebaldi Jun 13, 2024
40b2697
TASK: Convert MenuToggler to typescript
grebaldi Jun 14, 2024
ebaf71e
TASK: Decouple Drawer from redux store
grebaldi Jun 14, 2024
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
5 changes: 4 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ module.exports = {
'default-case': 'off',
'no-mixed-operators': 'off',
'no-negated-condition': 'off',
'complexity': 'off'
'complexity': 'off',

// This rule would prevent us from implementing meaningful value objects
'no-useless-constructor': 'off'
},
}
Binary file not shown.
Binary file not shown.
26 changes: 13 additions & 13 deletions Classes/Controller/BackendController.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@
use Neos\Neos\FrontendRouting\NodeAddressFactory;
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 @@ -48,12 +48,6 @@ class BackendController extends ActionController

protected $defaultViewObjectName = ApplicationView::class;

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

/**
* @Flow\Inject
* @var DomainRepository
Expand Down Expand Up @@ -114,6 +108,12 @@ class BackendController extends ActionController
*/
protected $menuProvider;

/**
* @Flow\Inject
* @var UserProviderInterface
*/
protected $userProvider;

/**
* @Flow\Inject
* @var InitialStateProviderInterface
Expand All @@ -139,11 +139,6 @@ public function indexAction(string $node = null)

$nodeAddress = $node !== null ? NodeAddressFactory::create($contentRepository)->createFromUriString($node) : null;
unset($node);
$user = $this->userService->getBackendUser();

if ($user === null) {
$this->redirectToUri($this->uriBuilder->uriFor('index', [], 'Login', 'Neos.Neos'));
}

$currentAccount = $this->securityContext->getAccount();
assert($currentAccount !== null);
Expand Down Expand Up @@ -185,6 +180,11 @@ public function indexAction(string $node = null)
$node = $subgraph->findNodeById($nodeAddress->nodeAggregateId);
}

$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 @@ -206,12 +206,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;
}
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
3 changes: 0 additions & 3 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 Down Expand Up @@ -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(),
],
];
}
}
14 changes: 14 additions & 0 deletions Classes/Presentation/ApplicationView.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

use Neos\Flow\Annotations as Flow;
use Neos\Flow\Core\Bootstrap;
use Neos\Flow\I18n\Cldr\Reader\PluralsReader;
use Neos\Flow\I18n\Locale;
use Neos\Flow\Mvc\View\AbstractView;
use Neos\Flow\ResourceManagement\ResourceManager;
use Neos\Flow\Security\Context as SecurityContext;
Expand Down Expand Up @@ -49,6 +51,9 @@ final class ApplicationView extends AbstractView
#[Flow\Inject]
protected Bootstrap $bootstrap;

#[Flow\Inject]
protected PluralsReader $pluralsReader;

/**
* This contains the supported options, their default values, descriptions and types.
*
Expand Down Expand Up @@ -113,6 +118,15 @@ private function renderHead(): string
)
);

$locale = new Locale($this->userService->getInterfaceLanguage());
// @TODO: All endpoints should be treated this way and be isolated from
// initial data.
$result .= sprintf(
'<link id="neos-ui-uri:/neos/xliff.json" rel="prefetch" href="%s" data-locale="%s" data-locale-plural-rules="%s">',
$this->variables['initialData']['configuration']['endpoints']['translations'],
(string) $locale,
implode(',', $this->pluralsReader->getPluralForms($locale)),
);
$result .= sprintf(
'<script id="initialData" type="application/json">%s</script>',
json_encode($this->variables['initialData']),
Expand Down
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"@neos-project/eslint-config-neos": "^2.6.1",
"@typescript-eslint/eslint-plugin": "^5.44.0",
"@typescript-eslint/parser": "^5.44.0",
"cross-fetch": "^4.0.0",
"editorconfig-checker": "^4.0.2",
"esbuild": "~0.17.0",
"eslint": "^8.27.0",
Expand Down
120 changes: 120 additions & 0 deletions packages/framework-observable-react/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# @neos-project/framework-observable-react

> React bindings for @neos-project/framework-observable

This package provides a set of React [hooks](https://react.dev/reference/react/hooks) to let components interact with `Observable`s.

## API

### `useLatestValueFrom`

```typescript
// Without default value:
function useLatestValueFrom<V>(observable$: Observable<V>): null | V;

// With default value:
function useLatestValueFrom<V, D>(
observable$: Observable<V>,
defaultValue: D
): D | V;
```

`useLatestValueFrom` is a way to bind a react component the latest value emitted from an `Observable`.

#### Parameters

| Name | Description |
| ------------------------- | ---------------------------------------------------------------------------------------------- |
| `observable$` | The `Observable` to subscribe to |
| `defaultValue` (optional) | The value to default for when `observable$` hasn't emitted any values yet (defaults to `null`) |

#### Return Value

This hook returns the latest value from the provided `observable$`. If no value has been emitted from the observable yet, it returns `defaultValue` which itself defaults to `null`.

#### Example

This component will display the amount of seconds that have passed since it was first mounted:

```typescript
const clock$ = createObservable((next) => {
let i = 1;
const interval = setInterval(() => {
next(i++);
}, 1000);

return () => clearInterval(interval);
});

const MyComponent = () => {
const seconds = useLatestValueFrom(clock$, 0);

return <pre>{seconds} seconds passed</pre>;
};
```

You can combine this with `React.useMemo`, if you wish to create an ad-hoc observable:

```typescript
const MyComponent = (props) => {
const beats = useLatestValueFrom(
React.useMemo(
() =>
createObservable((next) => {
let i = 1;
const interval = setInterval(() => {
next(i++);
}, props.millisecondsPerBeat);

return () => clearInterval(interval);
}),
[props.millisecondsPerBeat]
),
0
);

return <pre>{beats} beats passed</pre>;
};
```

### `useLatestState`

```typescript
function useLatestState<V>(state$: State<V>): V;
```

`useLatestState` subscribes to a given state observable and keeps track of its latest value.

#### Parameters

| Name | Description |
| -------- | --------------------------------------- |
| `state$` | The `State` observable to keep track of |

#### Return Value

This hook returns the latest value from the given `State` observable. Initially it contains the current value of the `State` at the moment the component was first mounted.

#### Example

```typescript
const count$ = createState(0);

const MyComponent = () => {
const count = useLatestState(count$);
const handleInc = React.useCallback(() => {
count$.update((count) => count + 1);
}, []);
const handleDec = React.useCallback(() => {
count$.update((count) => count - 1);
}, []);

return (
<div>
<pre>Count {count}</pre>
<button onClick={handleInc}>+</button>
<button onClick={handleDec}>-</button>
</div>
);
};
```
Loading
Loading