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

BUGFIX: Refactor node tree using Query DTOs #63

Merged
merged 36 commits into from
Jul 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
86bdf62
TASK: Update phpstan
grebaldi May 1, 2024
1abc6c7
FEATURE: Create query endpoints for node tree
grebaldi May 1, 2024
100ae82
TASK: Add nodeTypeLabel to TreeNode
grebaldi May 3, 2024
5e2a904
TASK: Prevent duplicate children in TreeNodeBuilder
grebaldi May 3, 2024
ef2f2ba
TASK: Split nodeTypeFilter into baseNodeTypeFilter and leafNodeTypeFi…
grebaldi May 3, 2024
d5ff3fd
TASK: Rename nodeTypename -> value in NodeTypeFilterOption
grebaldi May 3, 2024
cd73fb3
TASK: Split NodeTypeFilterOptions concern from GetTree and GetChildre…
grebaldi May 3, 2024
c29732d
TASK: Implement GetNodeSummary query
grebaldi May 6, 2024
4c01267
TASK: Ensure that root node can be matched by leaf node type filter
grebaldi May 6, 2024
4b6a516
TASK: Ensure that selected node is always in result set
grebaldi May 6, 2024
98eb815
TASK: Sort tree items by original node sorting index
grebaldi May 6, 2024
453000f
TASK: Adjust client code to use new query endpoints
grebaldi May 6, 2024
cdfcd31
TASK: Remove `useNodeSummary`
grebaldi May 6, 2024
f241c09
TASK: Remove `useHasNode`
grebaldi May 6, 2024
7a3a0c8
TASK: Remove `useNodeType`
grebaldi May 6, 2024
a6a2a34
TASK: Remove `useNodeTypes`
grebaldi May 6, 2024
c8efa81
TASK: Remove `useNodeTypesRegistry`
grebaldi May 6, 2024
116ccd3
TASK: Remove `INodeTypesRegistry`
grebaldi May 6, 2024
2b22a64
TASK: Remove `INodeType`
grebaldi May 6, 2024
a31113c
TASK: Remove `INodePartialForTree`
grebaldi May 6, 2024
1fcff91
TASK: Remove `INodeSummary`
grebaldi May 6, 2024
7919004
TASK: Remove `INode`
grebaldi May 6, 2024
1941263
TASK: Remove `NodeTypeName`
grebaldi May 6, 2024
2f99b01
TASK: Remove `searchNodes` endpoint
grebaldi May 6, 2024
de14b38
TASK: Remove FlowQuery from neos-bridge
grebaldi May 6, 2024
5c0e22e
TASK: Remove `uri` from `TreeNode` DTO
grebaldi May 6, 2024
aed3ad1
TASK: Rename `leafNodeTypeFilter` -> `narrowNodeTypeFilter`
grebaldi May 7, 2024
d9dadc7
TASK: Replicate `allowedNodeTypes` behavior introduced in #11
grebaldi May 7, 2024
520ba38
TASK: Remove `nodeTypeNames` from `TreeNode` DTO
grebaldi May 7, 2024
d47df56
TASK: Rename `TreeNodeBuilder::fromNode` -> `TreeNodeBuilder::forNode`
grebaldi May 7, 2024
5a44313
TASK: Add semantic errors and improve error handling
grebaldi May 7, 2024
21bced9
TASK: Translate node type label in `>SelectNodeTypeFilter/>`
grebaldi May 7, 2024
92d42f8
TASK: Use correct baseNodeTypeFilter for Node Link Editor
grebaldi May 7, 2024
29296d8
TASK: Ensure that empty linkableNodeTypes allow all node types to be …
grebaldi May 7, 2024
e702288
TASK: Correctly display `disabled` and `scheduledToBeDisabled` states…
grebaldi May 7, 2024
9771a80
TASK: Correctly consider configured `loadingDepth`
grebaldi May 7, 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
27 changes: 27 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
root = true

[*]
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
indent_style = space
indent_size = 4

[*.{yaml,yml}]
indent_size = 2

[{package.json,.babelrc,.eslintrc,.stylelintrc}]
indent_size = 2

[Makefile]
indent_style = tab

[*.makefile]
indent_style = tab

[*.md]
trim_trailing_whitespace = false

[*.yaml.example]
indent_size = 2
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

/*
* This script belongs to the package "Sitegeist.Archaeopteryx".
*
* 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 Sitegeist\Archaeopteryx\Application\GetChildrenForTreeNode\Controller;

use Neos\Flow\Annotations as Flow;
use Sitegeist\Archaeopteryx\Application\GetChildrenForTreeNode\GetChildrenForTreeNodeQuery;
use Sitegeist\Archaeopteryx\Application\GetChildrenForTreeNode\GetChildrenForTreeNodeQueryHandler;
use Sitegeist\Archaeopteryx\Application\Shared\NodeWasNotFound;
use Sitegeist\Archaeopteryx\Framework\MVC\QueryController;
use Sitegeist\Archaeopteryx\Framework\MVC\QueryResponse;

#[Flow\Scope("singleton")]
final class GetChildrenForTreeNodeController extends QueryController
{
#[Flow\Inject]
protected GetChildrenForTreeNodeQueryHandler $queryHandler;

public function processQuery(array $arguments): QueryResponse
{
try {
$query = GetChildrenForTreeNodeQuery::fromArray($arguments);
$queryResult = $this->queryHandler->handle($query);

return QueryResponse::success($queryResult);
} catch (NodeWasNotFound $e) {
return QueryResponse::clientError($e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

/*
* This script belongs to the package "Sitegeist.Archaeopteryx".
*
* 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 Sitegeist\Archaeopteryx\Application\GetChildrenForTreeNode;

use Neos\ContentRepository\Domain\NodeAggregate\NodeAggregateIdentifier;
use Neos\ContentRepository\Domain\NodeType\NodeTypeName;
use Neos\Flow\Annotations as Flow;
use Sitegeist\Archaeopteryx\Application\Shared\NodeTypeNames;

/**
* @internal
*/
#[Flow\Proxy(false)]
final class GetChildrenForTreeNodeQuery
{
/**
* @param array<string,array<int,string>> $dimensionValues
*/
public function __construct(
public readonly string $workspaceName,
public readonly array $dimensionValues,
public readonly NodeAggregateIdentifier $treeNodeId,
public readonly string $nodeTypeFilter,
public readonly NodeTypeNames $linkableNodeTypes,
) {
}

/**
* @param array<string,mixed> $array
*/
public static function fromArray(array $array): self
{
isset($array['workspaceName'])
or throw new \InvalidArgumentException('Workspace name must be set');
is_string($array['workspaceName'])
or throw new \InvalidArgumentException('Workspace name must be a string');

isset($array['dimensionValues'])
or throw new \InvalidArgumentException('Dimension values must be set');
is_array($array['dimensionValues'])
or throw new \InvalidArgumentException('Dimension values must be an array');

isset($array['treeNodeId'])
or throw new \InvalidArgumentException('Tree node id must be set');
is_string($array['treeNodeId'])
or throw new \InvalidArgumentException('Tree node id must be a string');

!isset($array['nodeTypeFilter']) or is_string($array['nodeTypeFilter'])
or throw new \InvalidArgumentException('Node type filter must be a string');

!isset($array['linkableNodeTypes']) or is_array($array['linkableNodeTypes'])
or throw new \InvalidArgumentException('Linkable node types must be an array');

return new self(
workspaceName: $array['workspaceName'],
dimensionValues: $array['dimensionValues'],
treeNodeId: NodeAggregateIdentifier::fromString($array['treeNodeId']),
nodeTypeFilter: $array['nodeTypeFilter'] ?? '',
linkableNodeTypes: NodeTypeNames::fromArray($array['linkableNodeTypes'] ?? []),
);
}

/**
* @return array<string,string>
*/
public function getTargetDimensionValues(): array
{
$result = [];

foreach ($this->dimensionValues as $dimensionName => $fallbackChain) {
$result[$dimensionName] = $fallbackChain[0] ?? '';
}

return $result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php

/*
* This script belongs to the package "Sitegeist.Archaeopteryx".
*
* 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 Sitegeist\Archaeopteryx\Application\GetChildrenForTreeNode;

use Neos\ContentRepository\Domain\Model\Node;
use Neos\ContentRepository\Domain\Service\NodeTypeManager;
use Neos\Flow\Annotations as Flow;
use Neos\Neos\Domain\Service\ContentContextFactory;
use Sitegeist\Archaeopteryx\Application\Shared\NodeWasNotFound;
use Sitegeist\Archaeopteryx\Application\Shared\TreeNodeBuilder;
use Sitegeist\Archaeopteryx\Application\Shared\TreeNodes;
use Sitegeist\Archaeopteryx\Infrastructure\ContentRepository\NodeTypeFilter;

/**
* @internal
*/
#[Flow\Scope("singleton")]
final class GetChildrenForTreeNodeQueryHandler
{
#[Flow\Inject]
protected ContentContextFactory $contentContextFactory;

#[Flow\Inject]
protected NodeTypeManager $nodeTypeManager;

public function handle(GetChildrenForTreeNodeQuery $query): GetChildrenForTreeNodeQueryResult
{
$contentContext = $this->contentContextFactory->create([
'workspaceName' => $query->workspaceName,
'dimensions' => $query->dimensionValues,
'targetDimensions' => $query->getTargetDimensionValues(),
'invisibleContentShown' => true,
'removedContentShown' => false,
'inaccessibleContentShown' => true
]);

$node = $contentContext->getNodeByIdentifier((string) $query->treeNodeId);
if (!$node instanceof Node) {
throw NodeWasNotFound::becauseNodeWithGivenIdentifierDoesNotExistInContext(
nodeAggregateIdentifier: $query->treeNodeId,
contentContext: $contentContext,
);
}

return new GetChildrenForTreeNodeQueryResult(
children: $this->createTreeNodesFromChildrenOfNode($node, $query),
);
}

private function createTreeNodesFromChildrenOfNode(Node $node, GetChildrenForTreeNodeQuery $query): TreeNodes
{
$linkableNodeTypesFilter = NodeTypeFilter::fromNodeTypeNames(
nodeTypeNames: $query->linkableNodeTypes,
nodeTypeManager: $this->nodeTypeManager
);

$items = [];

foreach ($node->getChildNodes($query->nodeTypeFilter) as $childNode) {
/** @var Node $childNode */
$items[] = TreeNodeBuilder::forNode($childNode)
->setIsMatchedByFilter(true)
->setIsLinkable($linkableNodeTypesFilter->isSatisfiedByNode($childNode))
->setHasUnloadedChildren($childNode->getNumberOfChildNodes($query->nodeTypeFilter) > 0)
->build();
}

return new TreeNodes(...$items);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

/*
* This script belongs to the package "Sitegeist.Archaeopteryx".
*
* 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 Sitegeist\Archaeopteryx\Application\GetChildrenForTreeNode;

use Neos\Flow\Annotations as Flow;
use Sitegeist\Archaeopteryx\Application\Shared\TreeNodes;

/**
* @internal
*/
#[Flow\Proxy(false)]
final class GetChildrenForTreeNodeQueryResult implements \JsonSerializable
{
public function __construct(
public readonly TreeNodes $children,
) {
}

public function jsonSerialize(): mixed
{
return get_object_vars($this);
}
}
33 changes: 33 additions & 0 deletions Classes/Application/GetNodeSummary/Breadcrumb.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

/*
* This script belongs to the package "Sitegeist.Archaeopteryx".
*
* 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 Sitegeist\Archaeopteryx\Application\GetNodeSummary;

use Neos\Flow\Annotations as Flow;

/**
* @internal
*/
#[Flow\Proxy(false)]
final class Breadcrumb implements \JsonSerializable
{
public function __construct(
public readonly string $icon,
public readonly string $label,
) {
}

public function jsonSerialize(): mixed
{
return get_object_vars($this);
}
}
35 changes: 35 additions & 0 deletions Classes/Application/GetNodeSummary/Breadcrumbs.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

/*
* This script belongs to the package "Sitegeist.Archaeopteryx".
*
* 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 Sitegeist\Archaeopteryx\Application\GetNodeSummary;

use Neos\Flow\Annotations as Flow;

/**
* @internal
*/
#[Flow\Proxy(false)]
final class Breadcrumbs implements \JsonSerializable
{
/** @var Breadcrumb[] */
private readonly array $items;

public function __construct(Breadcrumb ...$items)
{
$this->items = array_values($items);
}

public function jsonSerialize(): mixed
{
return $this->items;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

/*
* This script belongs to the package "Sitegeist.Archaeopteryx".
*
* 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 Sitegeist\Archaeopteryx\Application\GetNodeSummary\Controller;

use Neos\Flow\Annotations as Flow;
use Sitegeist\Archaeopteryx\Application\GetNodeSummary\GetNodeSummaryQuery;
use Sitegeist\Archaeopteryx\Application\GetNodeSummary\GetNodeSummaryQueryHandler;
use Sitegeist\Archaeopteryx\Application\Shared\NodeWasNotFound;
use Sitegeist\Archaeopteryx\Framework\MVC\QueryController;
use Sitegeist\Archaeopteryx\Framework\MVC\QueryResponse;

#[Flow\Scope("singleton")]
final class GetNodeSummaryController extends QueryController
{
#[Flow\Inject]
protected GetNodeSummaryQueryHandler $queryHandler;

public function processQuery(array $arguments): QueryResponse
{
try {
$query = GetNodeSummaryQuery::fromArray($arguments);
$queryResult = $this->queryHandler->handle($query);

return QueryResponse::success($queryResult);
} catch (NodeWasNotFound $e) {
return QueryResponse::clientError($e);
}
}
}
Loading
Loading