diff --git a/Classes/Domain/Model/Changes/AbstractCreate.php b/Classes/Domain/Model/Changes/AbstractCreate.php index 11ec715e95..03b5356c91 100644 --- a/Classes/Domain/Model/Changes/AbstractCreate.php +++ b/Classes/Domain/Model/Changes/AbstractCreate.php @@ -14,14 +14,21 @@ use Neos\ContentRepository\Core\ContentRepository; use Neos\ContentRepository\Core\DimensionSpace\OriginDimensionSpacePoint; +use Neos\ContentRepository\Core\Factory\ContentRepositoryServiceFactoryInterface; use Neos\ContentRepository\Core\Feature\NodeCreation\Command\CreateNodeAggregateWithNode; use Neos\ContentRepository\Core\NodeType\NodeTypeName; use Neos\ContentRepository\Core\Projection\ContentGraph\Node; use Neos\ContentRepository\Core\SharedModel\Exception\NodeNameIsAlreadyOccupied; use Neos\ContentRepository\Core\SharedModel\Node\NodeAggregateId; use Neos\ContentRepository\Core\SharedModel\Node\NodeName; +use Neos\Flow\Annotations as Flow; +use Neos\Flow\ObjectManagement\ObjectManagerInterface; +use Neos\Neos\Ui\Domain\NodeCreation\NodeCreationHandlerFactoryInterface; +use Neos\Neos\Ui\Domain\Service\NodePropertyConversionService; use Neos\Neos\Ui\Exception\InvalidNodeCreationHandlerException; -use Neos\Neos\Ui\NodeCreationHandler\NodeCreationHandlerInterface; +use Neos\Neos\Ui\Domain\NodeCreation\NodeCreationCommands; +use Neos\Neos\Ui\Domain\NodeCreation\NodeCreationElements; +use Neos\Neos\Ui\Domain\NodeCreation\NodeCreationHandlerInterface; use Neos\Utility\PositionalArraySorter; /** @@ -31,6 +38,16 @@ */ abstract class AbstractCreate extends AbstractStructuralChange { + /** + * @Flow\Inject + */ + protected ObjectManagerInterface $objectManager; + + /** + * @Flow\Inject + */ + protected NodePropertyConversionService $nodePropertyConversionService; + /** * The type of the node that will be created */ @@ -48,6 +65,11 @@ abstract class AbstractCreate extends AbstractStructuralChange */ protected ?string $name = null; + /** + * An (optional) node aggregate identifier that will be used for testing + */ + protected ?NodeAggregateId $nodeAggregateId = null; + /** * @param string $nodeTypeName */ @@ -88,6 +110,16 @@ public function getName(): ?string return $this->name; } + public function setNodeAggregateId(string $nodeAggregateId): void + { + $this->nodeAggregateId = NodeAggregateId::fromString($nodeAggregateId); + } + + public function getNodeAggregateId(): ?NodeAggregateId + { + return $this->nodeAggregateId; + } + /** * @param Node $parentNode * @param NodeAggregateId|null $succeedingSiblingNodeAggregateId @@ -106,7 +138,7 @@ protected function createNode( ? NodeName::fromString($this->getName()) : null; - $nodeAggregateId = NodeAggregateId::create(); // generate a new NodeAggregateId + $nodeAggregateId = $this->getNodeAggregateId() ?? NodeAggregateId::create(); // generate a new NodeAggregateId $contentRepository = $this->contentRepositoryRegistry->get($parentNode->subgraphIdentity->contentRepositoryId); $workspace = $this->contentRepositoryRegistry->get($this->subject->subgraphIdentity->contentRepositoryId) @@ -127,9 +159,24 @@ protected function createNode( $nodeName ); - $command = $this->applyNodeCreationHandlers($command, $nodeTypeName, $contentRepository); + $commands = $this->applyNodeCreationHandlers( + NodeCreationCommands::fromFirstCommand( + $command, + $contentRepository->getNodeTypeManager() + ), + $this->nodePropertyConversionService->convertNodeCreationElements( + $contentRepository->getNodeTypeManager()->getNodeType($nodeTypeName), + $this->getData() ?: [] + ), + $nodeTypeName, + $contentRepository + ); + + foreach ($commands as $command) { + $contentRepository->handle($command) + ->block(); + } - $contentRepository->handle($command)->block(); /** @var Node $newlyCreatedNode */ $newlyCreatedNode = $this->contentRepositoryRegistry->subgraphForNode($parentNode) ->findNodeById($nodeAggregateId); @@ -145,27 +192,45 @@ protected function createNode( * @throws InvalidNodeCreationHandlerException */ protected function applyNodeCreationHandlers( - CreateNodeAggregateWithNode $command, + NodeCreationCommands $commands, + NodeCreationElements $elements, NodeTypeName $nodeTypeName, ContentRepository $contentRepository - ): CreateNodeAggregateWithNode { - $data = $this->getData() ?: []; + ): NodeCreationCommands { $nodeType = $contentRepository->getNodeTypeManager()->getNodeType($nodeTypeName); if (!isset($nodeType->getOptions()['nodeCreationHandlers']) || !is_array($nodeType->getOptions()['nodeCreationHandlers'])) { - return $command; + return $commands; } - foreach ((new PositionalArraySorter($nodeType->getOptions()['nodeCreationHandlers']))->toArray() as $nodeCreationHandlerConfiguration) { - $nodeCreationHandler = new $nodeCreationHandlerConfiguration['nodeCreationHandler'](); + foreach ((new PositionalArraySorter($nodeType->getOptions()['nodeCreationHandlers']))->toArray() as $key => $nodeCreationHandlerConfiguration) { + if (!isset($nodeCreationHandlerConfiguration['factoryClassName'])) { + throw new InvalidNodeCreationHandlerException(sprintf( + 'Node creation handler "%s" has no "factoryClassName" specified.', + $key + ), 1697750190); + } + + $nodeCreationHandlerFactory = $this->objectManager->get($nodeCreationHandlerConfiguration['factoryClassName']); + if (!$nodeCreationHandlerFactory instanceof NodeCreationHandlerFactoryInterface) { + throw new InvalidNodeCreationHandlerException(sprintf( + 'Node creation handler "%s" didnt specify factory class of type %s. Got "%s"', + $key, + NodeCreationHandlerFactoryInterface::class, + get_class($nodeCreationHandlerFactory) + ), 1697750193); + } + + $nodeCreationHandler = $nodeCreationHandlerFactory->build($contentRepository); if (!$nodeCreationHandler instanceof NodeCreationHandlerInterface) { throw new InvalidNodeCreationHandlerException(sprintf( - 'Expected %s but got "%s"', + 'Node creation handler "%s" didnt specify factory class of type %s. Got "%s"', + $key, NodeCreationHandlerInterface::class, get_class($nodeCreationHandler) ), 1364759956); } - $command = $nodeCreationHandler->handle($command, $data, $contentRepository); + $commands = $nodeCreationHandler->handle($commands, $elements); } - return $command; + return $commands; } } diff --git a/Classes/Domain/Model/Changes/Property.php b/Classes/Domain/Model/Changes/Property.php index c9cff3c04c..fbf016ec48 100644 --- a/Classes/Domain/Model/Changes/Property.php +++ b/Classes/Domain/Model/Changes/Property.php @@ -195,8 +195,7 @@ public function apply(): void ); } else { $value = $this->nodePropertyConversionService->convert( - $this->getNodeType($subject), - $propertyName, + $this->getNodeType($subject)->getPropertyType($propertyName), $this->getValue() ); diff --git a/Classes/Domain/NodeCreation/NodeCreationCommands.php b/Classes/Domain/NodeCreation/NodeCreationCommands.php new file mode 100644 index 0000000000..b5d967bed2 --- /dev/null +++ b/Classes/Domain/NodeCreation/NodeCreationCommands.php @@ -0,0 +1,124 @@ +getContentGraph()->getSubgraph( + * $commands->first->contentStreamId, + * $commands->first->originDimensionSpacePoint->toDimensionSpacePoint(), + * VisibilityConstraints::frontend() + * ); + * $parentNode = $subgraph->findNodeById($commands->first->parentNodeAggregateId); + * + * @implements \IteratorAggregate + * @internal Especially the constructors + */ +final readonly class NodeCreationCommands implements \IteratorAggregate +{ + /** + * The initial node creation command. + * It is only allowed to change its properties via {@see self::withInitialPropertyValues()} + */ + public CreateNodeAggregateWithNode $first; + + /** + * Add a list of commands that are executed after the initial created command was run. + * This allows to create child-nodes and append other allowed commands. + * + * @var array + */ + public array $additionalCommands; + + private function __construct( + CreateNodeAggregateWithNode $first, + CreateNodeAggregateWithNode|SetNodeProperties|DisableNodeAggregate|EnableNodeAggregate|SetNodeReferences|CopyNodesRecursively ...$additionalCommands + ) { + $this->first = $first; + $this->additionalCommands = array_values($additionalCommands); + } + + /** + * @internal to guarantee that the initial create command is mostly preserved as intended. + * You can use {@see self::withInitialPropertyValues()} to add new properties of the to be created node. + */ + public static function fromFirstCommand( + CreateNodeAggregateWithNode $first, + NodeTypeManager $nodeTypeManager + ): self { + $tetheredDescendantNodeAggregateIds = NodeAggregateIdsByNodePaths::createForNodeType( + $first->nodeTypeName, + $nodeTypeManager + ); + return new self( + $first->withTetheredDescendantNodeAggregateIds($tetheredDescendantNodeAggregateIds), + ); + } + + /** + * Augment the first {@see CreateNodeAggregateWithNode} command with altered properties. + * + * The properties will be completely replaced. + * To merge the properties please use: + * + * $commands->withInitialPropertyValues( + * $commands->first->initialPropertyValues + * ->withValue('album', 'rep') + * ) + * + */ + public function withInitialPropertyValues(PropertyValuesToWrite $newInitialPropertyValues): self + { + return new self( + $this->first->withInitialPropertyValues($newInitialPropertyValues), + ...$this->additionalCommands + ); + } + + public function withAdditionalCommands( + CreateNodeAggregateWithNode|SetNodeProperties|DisableNodeAggregate|EnableNodeAggregate|SetNodeReferences|CopyNodesRecursively ...$additionalCommands + ): self { + return new self($this->first, ...$this->additionalCommands, ...$additionalCommands); + } + + /** + * @return \Traversable + */ + public function getIterator(): \Traversable + { + yield from [$this->first, ...$this->additionalCommands]; + } +} diff --git a/Classes/Domain/NodeCreation/NodeCreationElements.php b/Classes/Domain/NodeCreation/NodeCreationElements.php new file mode 100644 index 0000000000..8148c2112f --- /dev/null +++ b/Classes/Domain/NodeCreation/NodeCreationElements.php @@ -0,0 +1,96 @@ + + * @internal Especially the constructor and the serialized data + */ +final readonly class NodeCreationElements implements \IteratorAggregate +{ + /** + * @param array $elementValues + * @param array $serializedValues + * @internal you should not need to construct this + */ + public function __construct( + private array $elementValues, + private array $serializedValues, + ) { + } + + public function has(string $name): bool + { + return isset($this->elementValues[$name]); + } + + /** + * Returns the type according to the element schema + * For elements that refer to a node {@see NodeAggregateIds} will be returned. + */ + public function get(string $name): mixed + { + return $this->elementValues[$name] ?? null; + } + + /** + * @internal returns values formatted by the internal format used for the Ui + * @return \Traversable + */ + public function serialized(): \Traversable + { + yield from $this->serializedValues; + } + + /** + * @return \Traversable + */ + public function getIterator(): \Traversable + { + yield from $this->elementValues; + } +} diff --git a/Classes/Domain/NodeCreation/NodeCreationHandlerFactoryInterface.php b/Classes/Domain/NodeCreation/NodeCreationHandlerFactoryInterface.php new file mode 100644 index 0000000000..a20d84446b --- /dev/null +++ b/Classes/Domain/NodeCreation/NodeCreationHandlerFactoryInterface.php @@ -0,0 +1,14 @@ +|null $rawValue */ - public function convert(NodeType $nodeType, string $propertyName, string|array|null $rawValue): mixed + public function convert(string $propertyType, string|array|null $rawValue): mixed { - // WORKAROUND: $nodeType->getPropertyType() is missing the "initialize" call, - // so we need to trigger another method beforehand. - $nodeType->getFullConfiguration(); - $propertyType = $nodeType->getPropertyType($propertyName); - if (is_null($rawValue)) { return null; } + $propertyType = TypeHandling::normalizeType($propertyType); switch ($propertyType) { case 'string': return $rawValue; @@ -113,6 +111,35 @@ public function convert(NodeType $nodeType, string $propertyName, string|array|n } } + /** + * @param array $data + */ + public function convertNodeCreationElements(NodeType $nodeType, array $data): NodeCreationElements + { + $convertedElements = []; + /** @var string $elementName */ + foreach ($nodeType->getConfiguration('ui.creationDialog.elements') ?? [] as $elementName => $elementConfiguration) { + $rawValue = $data[$elementName] ?? null; + if ($rawValue === null) { + continue; + } + $propertyType = $elementConfiguration['type'] ?? 'string'; + if ($propertyType === 'references' || $propertyType === 'reference') { + $nodeAggregateIds = []; + if (is_string($rawValue) && !empty($rawValue)) { + $nodeAggregateIds = [$rawValue]; + } elseif (is_array($rawValue)) { + $nodeAggregateIds = $rawValue; + } + $convertedElements[$elementName] = NodeAggregateIds::fromArray($nodeAggregateIds); + continue; + } + $convertedElements[$elementName] = $this->convert($propertyType, $rawValue); + } + + return new NodeCreationElements(elementValues: $convertedElements, serializedValues: $data); + } + /** * Convert raw value to \DateTime * diff --git a/Classes/Infrastructure/ContentRepository/CreationDialog/CreationDialogNodeTypePostprocessor.php b/Classes/Infrastructure/ContentRepository/CreationDialog/CreationDialogNodeTypePostprocessor.php new file mode 100644 index 0000000000..d0edd45d52 --- /dev/null +++ b/Classes/Infrastructure/ContentRepository/CreationDialog/CreationDialogNodeTypePostprocessor.php @@ -0,0 +1,227 @@ + + * @Flow\InjectConfiguration(package="Neos.Neos", path="userInterface.inspector.dataTypes") + */ + protected $dataTypesDefaultConfiguration; + + /** + * @var array + * @Flow\InjectConfiguration(package="Neos.Neos", path="userInterface.inspector.editors") + */ + protected $editorDefaultConfiguration; + + /** + * @param NodeType $nodeType (uninitialized) The node type to process + * @param array $configuration input configuration + * @param array $options The processor options + * @return void + */ + public function process(NodeType $nodeType, array &$configuration, array $options): void + { + $creationDialogElements = $configuration['ui']['creationDialog']['elements'] ?? []; + + if (!empty($configuration['properties'] ?? null)) { + $creationDialogElements = $this->promotePropertiesIntoCreationDialog($configuration['properties'], $creationDialogElements); + } + + $this->mergeDefaultCreationDialogElementEditors($creationDialogElements); + + if ($creationDialogElements !== []) { + $configuration['ui']['creationDialog']['elements'] = (new PositionalArraySorter($creationDialogElements))->toArray(); + } + } + + /** + * @param array $creationDialogElements + */ + private function mergeDefaultCreationDialogElementEditors(array &$creationDialogElements): void + { + foreach ($creationDialogElements as &$elementConfiguration) { + if (!isset($elementConfiguration['type'])) { + continue; + } + + $type = $elementConfiguration['type']; + $defaultConfigurationFromDataType = $this->dataTypesDefaultConfiguration[$type] ?? []; + + // FIRST STEP: Figure out which editor should be used + // - Default: editor as configured from the data type + // - Override: editor as configured from the property configuration. + if (isset($elementConfiguration['ui']['editor'])) { + $editor = $elementConfiguration['ui']['editor']; + } elseif (isset($defaultConfigurationFromDataType['editor'])) { + $editor = $defaultConfigurationFromDataType['editor']; + } else { + // No exception since the configuration could be a partial configuration overriding a property + // with showInCreationDialog flag set + continue; + } + + // SECOND STEP: Build up the full UI configuration by merging: + // - take configuration from editor defaults + // - take configuration from dataType + // - take configuration from creationDialog elements (NodeTypes) + $mergedUiConfiguration = $this->editorDefaultConfiguration[$editor] ?? []; + $mergedUiConfiguration = Arrays::arrayMergeRecursiveOverrule( + $mergedUiConfiguration, + $defaultConfigurationFromDataType + ); + $mergedUiConfiguration = Arrays::arrayMergeRecursiveOverrule( + $mergedUiConfiguration, + $elementConfiguration['ui'] ?? [] + ); + $elementConfiguration['ui'] = $mergedUiConfiguration; + $elementConfiguration['ui']['editor'] = $editor; + } + } + + /** + * @param array $properties + * @param array $explicitCreationDialogElements + * @return array + */ + private function promotePropertiesIntoCreationDialog(array $properties, array $explicitCreationDialogElements): array + { + foreach ($properties as $propertyName => $propertyConfiguration) { + if ( + !isset($propertyConfiguration['ui']['showInCreationDialog']) + || $propertyConfiguration['ui']['showInCreationDialog'] !== true + ) { + continue; + } + $creationDialogElement = $this->promotePropertyIntoCreationDialog($propertyName, $propertyConfiguration); + if (isset($explicitCreationDialogElements[$propertyName])) { + $creationDialogElement = Arrays::arrayMergeRecursiveOverrule( + $creationDialogElement, + $explicitCreationDialogElements[$propertyName] + ); + } + $explicitCreationDialogElements[$propertyName] = $creationDialogElement; + } + return $explicitCreationDialogElements; + } + + /** + * Converts a NodeType property configuration to the corresponding creationDialog "element" configuration + * + * @param string $propertyName + * @param array $propertyConfiguration + * @return array + */ + private function promotePropertyIntoCreationDialog(string $propertyName, array $propertyConfiguration): array + { + $dataType = $propertyConfiguration['type'] ?? 'string'; + $dataTypeDefaultConfiguration = $this->dataTypesDefaultConfiguration[$dataType] ?? []; + $convertedConfiguration = [ + 'type' => $dataType, + 'ui' => [ + 'label' => $propertyConfiguration['ui']['label'] ?? $propertyName, + ], + ]; + if (isset($propertyConfiguration['defaultValue'])) { + $convertedConfiguration['defaultValue'] = $propertyConfiguration['defaultValue']; + } + if (isset($propertyConfiguration['ui']['help'])) { + $convertedConfiguration['ui']['help'] = $propertyConfiguration['ui']['help']; + } + if (isset($propertyConfiguration['validation'])) { + $convertedConfiguration['validation'] = $propertyConfiguration['validation']; + } + if (isset($propertyConfiguration['ui']['inspector']['position'])) { + $convertedConfiguration['position'] = $propertyConfiguration['ui']['inspector']['position']; + } + if (isset($propertyConfiguration['ui']['inspector']['hidden'])) { + $convertedConfiguration['ui']['hidden'] = $propertyConfiguration['ui']['inspector']['hidden']; + } + + // todo maybe duplicated due to mergeDefaultCreationDialogElementEditors + $editor = $propertyConfiguration['ui']['inspector']['editor'] + ?? $dataTypeDefaultConfiguration['editor'] + ?? 'Neos.Neos/Inspector/Editors/TextFieldEditor'; + $editorOptions = $propertyConfiguration['ui']['inspector']['editorOptions'] ?? []; + if (isset($dataTypeDefaultConfiguration['editorOptions'])) { + $editorOptions = Arrays::arrayMergeRecursiveOverrule( + $dataTypeDefaultConfiguration['editorOptions'], + $editorOptions + ); + } + if (isset($this->editorDefaultConfiguration[$editor]['editorOptions'])) { + $editorOptions = Arrays::arrayMergeRecursiveOverrule( + $this->editorDefaultConfiguration[$editor]['editorOptions'], + $editorOptions + ); + } + + $convertedConfiguration['ui']['editor'] = $editor; + $convertedConfiguration['ui']['editorOptions'] = $editorOptions; + return $convertedConfiguration; + } +} diff --git a/Classes/Infrastructure/ContentRepository/CreationDialog/PromotedElementsCreationHandlerFactory.php b/Classes/Infrastructure/ContentRepository/CreationDialog/PromotedElementsCreationHandlerFactory.php new file mode 100644 index 0000000000..64993e8359 --- /dev/null +++ b/Classes/Infrastructure/ContentRepository/CreationDialog/PromotedElementsCreationHandlerFactory.php @@ -0,0 +1,80 @@ +getNodeTypeManager()) implements NodeCreationHandlerInterface { + public function __construct( + private readonly NodeTypeManager $nodeTypeManager + ) { + } + + public function handle(NodeCreationCommands $commands, NodeCreationElements $elements): NodeCreationCommands + { + $nodeType = $this->nodeTypeManager->getNodeType($commands->first->nodeTypeName); + + $propertyValues = $commands->first->initialPropertyValues; + $setReferencesCommands = []; + foreach ($elements as $elementName => $elementValue) { + // handle properties + // todo this will be simplified once hasProperty does not return true for references + if ($nodeType->hasProperty($elementName) && ($nodeType->getPropertyType($elementName) !== 'references' && $nodeType->getPropertyType($elementName) !== 'reference')) { + $propertyConfiguration = $nodeType->getProperties()[$elementName]; + if ( + ($propertyConfiguration['ui']['showInCreationDialog'] ?? false) === true + ) { + // a promoted element + $propertyValues = $propertyValues->withValue($elementName, $elementValue); + } + } + + // handle references + // todo this will be replaced by $nodeType->hasReference() + if ($nodeType->hasProperty($elementName) && ($nodeType->getPropertyType($elementName) === 'references' || $nodeType->getPropertyType($elementName) === 'reference')) { + assert($elementValue instanceof NodeAggregateIds); + $referenceConfiguration = $nodeType->getProperties()[$elementName]; + if ( + ($referenceConfiguration['ui']['showInCreationDialog'] ?? false) === true + ) { + // a promoted element + $setReferencesCommands[] = SetNodeReferences::create( + $commands->first->workspaceName, + $commands->first->nodeAggregateId, + $commands->first->originDimensionSpacePoint, + ReferenceName::fromString($elementName), + NodeReferencesToWrite::fromNodeAggregateIds($elementValue) + ); + } + } + } + + return $commands + ->withInitialPropertyValues($propertyValues) + ->withAdditionalCommands(...$setReferencesCommands); + } + }; + } +} diff --git a/Classes/Infrastructure/Neos/UriPathSegmentNodeCreationHandlerFactory.php b/Classes/Infrastructure/Neos/UriPathSegmentNodeCreationHandlerFactory.php new file mode 100644 index 0000000000..c646b1c48c --- /dev/null +++ b/Classes/Infrastructure/Neos/UriPathSegmentNodeCreationHandlerFactory.php @@ -0,0 +1,89 @@ +getNodeTypeManager(), $this->transliterationService) implements NodeCreationHandlerInterface { + public function __construct( + private readonly NodeTypeManager $nodeTypeManager, + private readonly TransliterationService $transliterationService + ) { + } + + public function handle(NodeCreationCommands $commands, NodeCreationElements $elements): NodeCreationCommands + { + if ( + !$this->nodeTypeManager->getNodeType($commands->first->nodeTypeName) + ->isOfType('Neos.Neos:Document') + ) { + return $commands; + } + + // if specified, the uriPathSegment equals the title + $uriPathSegment = $elements->get('title'); + + // if not empty, we transliterate the uriPathSegment according to the language of the new node + if ($uriPathSegment !== null && $uriPathSegment !== '') { + $uriPathSegment = $this->transliterateText( + $commands->first->originDimensionSpacePoint->toDimensionSpacePoint(), + $uriPathSegment + ); + } else { + // alternatively we set it to a random string like `document-blog-022` + $nodeTypeSuffix = explode(':', $commands->first->nodeTypeName->value)[1] ?? ''; + $uriPathSegment = sprintf('%s-%03d', $nodeTypeSuffix, random_int(0, 999)); + } + $uriPathSegment = Transliterator::urlize($uriPathSegment); + $propertyValues = $commands->first->initialPropertyValues->withValue('uriPathSegment', $uriPathSegment); + + return $commands->withInitialPropertyValues($propertyValues); + } + + private function transliterateText(DimensionSpacePoint $dimensionSpacePoint, string $text): string + { + $languageDimensionValue = $dimensionSpacePoint->getCoordinate(new ContentDimensionId('language')); + if ($languageDimensionValue !== null) { + try { + $language = (new Locale($languageDimensionValue))->getLanguage(); + } catch (InvalidLocaleIdentifierException $e) { + // we don't need to do anything here; we'll just transliterate the text. + } + } + return $this->transliterationService->transliterate($text, $language ?? null); + } + }; + } +} diff --git a/Classes/NodeCreationHandler/ContentTitleNodeCreationHandler.php b/Classes/NodeCreationHandler/ContentTitleNodeCreationHandler.php deleted file mode 100644 index cd2f68fa16..0000000000 --- a/Classes/NodeCreationHandler/ContentTitleNodeCreationHandler.php +++ /dev/null @@ -1,59 +0,0 @@ - $data incoming data from the creationDialog - * @throws NodeTypeNotFoundException - */ - public function handle(CreateNodeAggregateWithNode $command, array $data, ContentRepository $contentRepository): CreateNodeAggregateWithNode - { - if ( - !$contentRepository->getNodeTypeManager()->getNodeType($command->nodeTypeName) - ->isOfType('Neos.Neos:Content') - ) { - return $command; - } - - $propertyValues = $command->initialPropertyValues; - if (isset($data['title'])) { - $propertyValues = $propertyValues->withValue('title', $data['title']); - } - - return $command->withInitialPropertyValues($propertyValues); - } -} diff --git a/Classes/NodeCreationHandler/CreationDialogPropertiesCreationHandler.php b/Classes/NodeCreationHandler/CreationDialogPropertiesCreationHandler.php deleted file mode 100644 index dc47e42469..0000000000 --- a/Classes/NodeCreationHandler/CreationDialogPropertiesCreationHandler.php +++ /dev/null @@ -1,68 +0,0 @@ - $data - */ - public function handle(CreateNodeAggregateWithNode $command, array $data, ContentRepository $contentRepository): CreateNodeAggregateWithNode - { - $propertyMappingConfiguration = $this->propertyMapper->buildPropertyMappingConfiguration(); - $propertyMappingConfiguration->forProperty('*')->allowAllProperties(); - $propertyMappingConfiguration->setTypeConverterOption(PersistentObjectConverter::class, PersistentObjectConverter::CONFIGURATION_OVERRIDE_TARGET_TYPE_ALLOWED, true); - - $nodeType = $contentRepository->getNodeTypeManager()->getNodeType($command->nodeTypeName); - $propertyValues = $command->initialPropertyValues; - foreach ($nodeType->getConfiguration('properties') as $propertyName => $propertyConfiguration) { - if ( - !isset($propertyConfiguration['ui']['showInCreationDialog']) - || $propertyConfiguration['ui']['showInCreationDialog'] !== true - ) { - continue; - } - $propertyType = TypeHandling::normalizeType($propertyConfiguration['type'] ?? 'string'); - if (!isset($data[$propertyName])) { - continue; - } - $propertyValue = $data[$propertyName]; - if ($propertyType !== 'references' && $propertyType !== 'reference' && $propertyType !== TypeHandling::getTypeForValue($propertyValue)) { - $propertyValue = $this->propertyMapper->convert($propertyValue, $propertyType, $propertyMappingConfiguration); - } - - $propertyValues = $propertyValues->withValue($propertyName, $propertyValue); - } - - return $command->withInitialPropertyValues($propertyValues); - } -} diff --git a/Classes/NodeCreationHandler/DocumentTitleNodeCreationHandler.php b/Classes/NodeCreationHandler/DocumentTitleNodeCreationHandler.php deleted file mode 100644 index 8efe069f2f..0000000000 --- a/Classes/NodeCreationHandler/DocumentTitleNodeCreationHandler.php +++ /dev/null @@ -1,95 +0,0 @@ - $data - */ - public function handle(CreateNodeAggregateWithNode $command, array $data, ContentRepository $contentRepository): CreateNodeAggregateWithNode - { - if ( - !$contentRepository->getNodeTypeManager()->getNodeType($command->nodeTypeName) - ->isOfType('Neos.Neos:Document') - ) { - return $command; - } - $propertyValues = $command->initialPropertyValues; - if (isset($data['title'])) { - $propertyValues = $propertyValues->withValue('title', $data['title']); - } - - // if specified, the uriPathSegment equals the title - $uriPathSegment = $data['title']; - - // otherwise, we fall back to the node name - if ($uriPathSegment === null && $command->nodeName !== null) { - $uriPathSegment = $command->nodeName->value; - } - - // if not empty, we transliterate the uriPathSegment according to the language of the new node - if ($uriPathSegment !== null && $uriPathSegment !== '') { - $uriPathSegment = $this->transliterateText( - $command->originDimensionSpacePoint->toDimensionSpacePoint(), - $uriPathSegment - ); - } else { - // alternatively we set it to a random string - $uriPathSegment = uniqid('', true); - } - $uriPathSegment = Transliterator::urlize($uriPathSegment); - $propertyValues = $propertyValues->withValue('uriPathSegment', $uriPathSegment); - - return $command->withInitialPropertyValues($propertyValues); - } - - private function transliterateText(DimensionSpacePoint $dimensionSpacePoint, string $text): string - { - $languageDimensionValue = $dimensionSpacePoint->getCoordinate(new ContentDimensionId('language')); - if ($languageDimensionValue !== null) { - try { - $language = (new Locale($languageDimensionValue))->getLanguage(); - } catch (InvalidLocaleIdentifierException $e) { - // we don't need to do anything here; we'll just transliterate the text. - } - } - return $this->transliterationService->transliterate($text, $language ?? null); - } -} diff --git a/Classes/NodeCreationHandler/NodeCreationHandlerInterface.php b/Classes/NodeCreationHandler/NodeCreationHandlerInterface.php deleted file mode 100644 index b51ed2c408..0000000000 --- a/Classes/NodeCreationHandler/NodeCreationHandlerInterface.php +++ /dev/null @@ -1,32 +0,0 @@ - $data incoming data from the creationDialog - * @return CreateNodeAggregateWithNode the original command or a new creation command with altered properties - */ - public function handle(CreateNodeAggregateWithNode $command, array $data, ContentRepository $contentRepository): CreateNodeAggregateWithNode; -} diff --git a/Classes/Service/PublishingService.php b/Classes/Service/PublishingService.php index d6822643a3..d102fe8370 100644 --- a/Classes/Service/PublishingService.php +++ b/Classes/Service/PublishingService.php @@ -23,7 +23,6 @@ /** * A generic ContentRepository Publishing Service * - * @api * @Flow\Scope("singleton") * @internal */ diff --git a/Configuration/NodeTypes.yaml b/Configuration/NodeTypes.yaml index d5330a57ec..d8d0c108ae 100644 --- a/Configuration/NodeTypes.yaml +++ b/Configuration/NodeTypes.yaml @@ -1,15 +1,17 @@ 'Neos.Neos:Document': + postprocessors: + 'CreationDialogPostprocessor': + position: 'after NodeTypePresetPostprocessor' + postprocessor: 'Neos\Neos\Ui\Infrastructure\ContentRepository\CreationDialog\CreationDialogNodeTypePostprocessor' ui: creationDialog: elements: title: - type: string - ui: - label: i18n - editor: 'Neos.Neos/Inspector/Editors/TextFieldEditor' - validation: - 'Neos.Neos/Validation/NotEmptyValidator': {} + position: 'start' properties: + title: + ui: + showInCreationDialog: true uriPathSegment: ui: inspector: @@ -18,14 +20,20 @@ title: "ClientEval:node.properties.title" options: nodeCreationHandlers: - documentTitle: - nodeCreationHandler: 'Neos\Neos\Ui\NodeCreationHandler\DocumentTitleNodeCreationHandler' + uriPathSegment: + factoryClassName: 'Neos\Neos\Ui\Infrastructure\Neos\UriPathSegmentNodeCreationHandlerFactory' + promotedElements: + factoryClassName: 'Neos\Neos\Ui\Infrastructure\ContentRepository\CreationDialog\PromotedElementsCreationHandlerFactory' 'Neos.Neos:Content': + postprocessors: + 'CreationDialogPostprocessor': + position: 'after NodeTypePresetPostprocessor' + postprocessor: 'Neos\Neos\Ui\Infrastructure\ContentRepository\CreationDialog\CreationDialogNodeTypePostprocessor' options: nodeCreationHandlers: - documentTitle: - nodeCreationHandler: 'Neos\Neos\Ui\NodeCreationHandler\ContentTitleNodeCreationHandler' + promotedElements: + factoryClassName: 'Neos\Neos\Ui\Infrastructure\ContentRepository\CreationDialog\PromotedElementsCreationHandlerFactory' 'Neos.Neos:ContentCollection': ui: diff --git a/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/Classes/NodeCreationHandler/ImagePropertyNodeCreationHandler.php b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/Classes/NodeCreationHandler/ImagePropertyNodeCreationHandler.php deleted file mode 100644 index c12639ebf4..0000000000 --- a/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/Classes/NodeCreationHandler/ImagePropertyNodeCreationHandler.php +++ /dev/null @@ -1,43 +0,0 @@ -propertyMapper->buildPropertyMappingConfiguration(); - $propertyMappingConfiguration->forProperty('*')->allowAllProperties(); - $propertyMappingConfiguration->setTypeConverterOption(PersistentObjectConverter::class, PersistentObjectConverter::CONFIGURATION_OVERRIDE_TARGET_TYPE_ALLOWED, true); - $image = $this->propertyMapper->convert($data['image'], ImageInterface::class, $propertyMappingConfiguration); - - $propertyValues = $command->initialPropertyValues->withValue('image', $image); - return $command->withInitialPropertyValues($propertyValues); - } -} diff --git a/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/NodeTypes/Document/PageWithImage.yaml b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/NodeTypes/Document/PageWithImage.yaml index 5fae514863..320dbc4e14 100644 --- a/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/NodeTypes/Document/PageWithImage.yaml +++ b/Tests/IntegrationTests/TestDistribution/DistributionPackages/Neos.TestNodeTypes/NodeTypes/Document/PageWithImage.yaml @@ -1,56 +1,10 @@ 'Neos.TestNodeTypes:Document.PageWithImage': superTypes: 'Neos.Neos:Document': true - options: - nodeCreationHandlers: - image: - nodeCreationHandler: 'Neos\TestNodeTypes\NodeCreationHandler\ImagePropertyNodeCreationHandler' ui: label: PageWithImage_Test icon: icon-file-o position: 100 - creationDialog: - elements: - image: - type: Neos\Media\Domain\Model\ImageInterface - ui: - label: Image - editor: Neos.Neos/Inspector/Editors/ImageEditor - editorOptions: - fileUploadLabel: Neos.Neos:Main:choose - maximumFileSize: - features: - crop: true - upload: true - mediaBrowser: true - resize: false - crop: - aspectRatio: - options: - square: - width: 1 - height: 1 - label: Square - fourFive: - width: 4 - height: 5 - fiveSeven: - width: 5 - height: 7 - twoThree: - width: 2 - height: 3 - fourThree: - width: 4 - height: 3 - sixteenNine: - width: 16 - height: 9 - enableOriginal: true - allowCustom: true - locked: - width: 1 - height: 1 childNodes: main: type: 'Neos.Neos:ContentCollection' @@ -60,6 +14,7 @@ ui: label: 'Image' reloadIfChanged: true + showInCreationDialog: true inspector: group: 'document' editorOptions: diff --git a/Tests/IntegrationTests/TestDistribution/composer.json b/Tests/IntegrationTests/TestDistribution/composer.json index 97773a76be..428860f439 100644 --- a/Tests/IntegrationTests/TestDistribution/composer.json +++ b/Tests/IntegrationTests/TestDistribution/composer.json @@ -32,6 +32,9 @@ }, "extra": { "patches": { + "neos/neos-development-collection": { + "TASK: Remove declaration of ui nodeCreationHandlers": "https://github.com/neos/neos-development-collection/pull/4630.patch" + } } }, "repositories": { diff --git a/Tests/Unit/CreationDialogNodeTypePostprocessorTest.php b/Tests/Unit/CreationDialogNodeTypePostprocessorTest.php new file mode 100644 index 0000000000..38bf059b00 --- /dev/null +++ b/Tests/Unit/CreationDialogNodeTypePostprocessorTest.php @@ -0,0 +1,423 @@ +postprocessor = new CreationDialogNodeTypePostprocessor(); + $this->mockNodeType = new NodeType(NodeTypeName::fromString('Foo:Bar'), [], [], new DefaultNodeLabelGeneratorFactory()); + } + + /** + * promoted elements (showInCreationDialog: true) + * + * @test + */ + public function processCopiesInspectorConfigurationToCreationDialogElements(): void + { + $configuration = [ + 'properties' => [ + 'somePropertyName' => [ + 'ui' => [ + 'showInCreationDialog' => true, + 'inspector' => [ + 'position' => 123, + 'editor' => 'Some\Editor', + 'editorOptions' => ['some' => 'option'], + 'hidden' => 'ClientEval:false' + ], + ], + 'validation' => [ + 'Neos.Neos/Validation/NotEmptyValidator' => [], + 'Neos.Neos/Validation/StringLengthValidator' => [ + 'minimum' => 1, + 'maximum' => 255, + ] + ], + ], + ], + ]; + + $this->postprocessor->process($this->mockNodeType, $configuration, []); + + $expectedElements = [ + 'somePropertyName' => [ + 'type' => 'string', + 'ui' => [ + 'label' => 'somePropertyName', + 'hidden' => 'ClientEval:false', + 'editor' => 'Some\Editor', + 'editorOptions' => ['some' => 'option'], + ], + 'validation' => [ + 'Neos.Neos/Validation/NotEmptyValidator' => [], + 'Neos.Neos/Validation/StringLengthValidator' => [ + 'minimum' => 1, + 'maximum' => 255, + ] + ], + 'position' => 123, + ], + ]; + + self::assertSame($expectedElements, $configuration['ui']['creationDialog']['elements']); + } + + /** + * @test + */ + public function processDoesNotCreateEmptyCreationDialogs(): void + { + $configuration = [ + 'properties' => [ + 'somePropertyName' => [ + 'ui' => [ + 'inspector' => [ + 'editor' => 'Some\Editor', + 'editorOptions' => ['some' => 'option'], + ], + ], + ], + ], + ]; + $originalConfiguration = $configuration; + + $this->postprocessor->process($this->mockNodeType, $configuration, []); + + self::assertSame($originalConfiguration, $configuration); + } + + /** + * promoted elements (showInCreationDialog: true) + * + * @test + */ + public function processRespectsDataTypeDefaultConfiguration(): void + { + $configuration = [ + 'properties' => [ + 'somePropertyName' => [ + 'type' => 'SomeType', + 'ui' => [ + 'label' => 'Some Label', + 'showInCreationDialog' => true, + 'inspector' => [ + 'editorOptions' => ['some' => 'option'], + ], + ], + ], + ], + ]; + $this->inject($this->postprocessor, 'dataTypesDefaultConfiguration', [ + 'SomeType' => [ + 'editor' => 'Some\Default\Editor', + 'editorOptions' => [ + 'some' => 'defaultOption', + 'someDefault' => 'option', + ] + ] + ]); + + $this->postprocessor->process($this->mockNodeType, $configuration, []); + + $expectedElements = [ + 'somePropertyName' => [ + 'type' => 'SomeType', + 'ui' => [ + 'label' => 'Some Label', + 'editor' => 'Some\Default\Editor', + 'editorOptions' => ['some' => 'option', 'someDefault' => 'option'], + ], + ], + ]; + + self::assertEquals($expectedElements, $configuration['ui']['creationDialog']['elements']); + } + + /** + * promoted elements (showInCreationDialog: true) + * + * @test + */ + public function processRespectsEditorDefaultConfiguration(): void + { + $configuration = [ + 'properties' => [ + 'somePropertyName' => [ + 'type' => 'SomeType', + 'ui' => [ + 'showInCreationDialog' => true, + 'inspector' => [ + 'editorOptions' => ['some' => 'option'], + ], + ], + ], + ], + ]; + $this->inject($this->postprocessor, 'editorDefaultConfiguration', [ + 'Some\Editor' => [ + 'editorOptions' => [ + 'some' => 'editorDefault', + 'someDefault' => 'fromEditor', + 'someEditorDefault' => 'fromEditor', + ] + ] + ]); + $this->inject($this->postprocessor, 'dataTypesDefaultConfiguration', [ + 'SomeType' => [ + 'editor' => 'Some\Editor', + 'editorOptions' => [ + 'some' => 'defaultOption', + 'someDefault' => 'fromDataType', + ] + ] + ]); + + + $this->postprocessor->process($this->mockNodeType, $configuration, []); + + $expectedElements = [ + 'somePropertyName' => [ + 'type' => 'SomeType', + 'ui' => [ + 'label' => 'somePropertyName', + 'editor' => 'Some\Editor', + 'editorOptions' => ['some' => 'option', 'someDefault' => 'fromDataType', 'someEditorDefault' => 'fromEditor'], + ], + ], + ]; + + self::assertEquals($expectedElements, $configuration['ui']['creationDialog']['elements']); + } + + /** + * default editor + * + * @test + */ + public function processConvertsCreationDialogConfiguration(): void + { + $configuration = [ + 'ui' => [ + 'creationDialog' => [ + 'elements' => [ + 'elementWithoutType' => [ + 'ui' => [ + 'label' => 'Some Label' + ] + ], + 'elementWithUnknownType' => [ + 'type' => 'TypeWithoutDataTypeConfig', + 'ui' => [ + 'label' => 'Some Label', + 'editor' => 'EditorFromPropertyConfig', + ] + ], + 'elementWithEditorFromDataTypeConfig' => [ + 'type' => 'TypeWithDataTypeConfig', + 'ui' => [ + 'value' => 'fromPropertyConfig', + 'elementValue' => 'fromPropertyConfig', + ] + ], + 'elementWithEditorFromDataTypeConfigWithoutUiConfig' => [ + 'type' => 'TypeWithDataTypeConfig' + ], + 'elementWithOverriddenEditorConfig' => [ + 'type' => 'TypeWithDataTypeConfig', + 'ui' => [ + 'editor' => 'EditorFromPropertyConfig', + 'value' => 'fromPropertyConfig', + 'elementValue' => 'fromPropertyConfig', + ] + ], + 'elementWithOverriddenEditorConfigAndEditorDefaultConfig' => [ + 'type' => 'TypeWithDataTypeConfig', + 'ui' => [ + 'editor' => 'EditorWithDefaultConfig', + 'value' => 'fromPropertyConfig', + 'elementValue' => 'fromPropertyConfig', + ] + ], + 'elementWithEditorDefaultConfig' => [ + 'type' => 'TypeWithDefaultEditorConfig', + 'ui' => [ + 'value' => 'fromPropertyConfig', + 'elementValue' => 'fromPropertyConfig', + ] + ], + 'elementWithOverriddenEditorConfigAndEditorDefaultConfig2' => [ + 'type' => 'TypeWithDefaultEditorConfig', + 'ui' => [ + 'editor' => 'EditorWithoutDefaultConfig', + 'elementValue' => 'fromPropertyConfig', + ] + ], + 'elementWithOverriddenEditorConfigAndEditorDefaultConfig3' => [ + 'type' => 'TypeWithDefaultEditorConfig2', + 'ui' => [ + 'editor' => 'EditorWithDefaultConfig', + 'elementValue' => 'fromPropertyConfig', + ] + ], + ], + ], + ], + ]; + + $this->inject($this->postprocessor, 'editorDefaultConfiguration', [ + 'EditorWithDefaultConfig' => [ + 'value' => 'fromEditorDefaultConfig', + 'editorDefaultValue' => 'fromEditorDefaultConfig', + ], + ]); + + $this->inject($this->postprocessor, 'dataTypesDefaultConfiguration', [ + 'TypeWithDataTypeConfig' => [ + 'editor' => 'EditorFromDataTypeConfig', + 'value' => 'fromDataTypeConfig', + 'dataTypeValue' => 'fromDataTypeConfig', + ], + 'TypeWithDefaultEditorConfig' => [ + 'editor' => 'EditorWithDefaultConfig', + 'value' => 'fromDataTypeConfig', + 'dataTypeValue' => 'fromDataTypeConfig', + ], + 'TypeWithDefaultEditorConfig2' => [ + 'editor' => 'EditorWithDefaultConfig', + 'dataTypeValue' => 'fromDataTypeConfig', + ], + ]); + + $expectedResult = [ + 'ui' => [ + 'creationDialog' => [ + 'elements' => [ + 'elementWithoutType' => [ + 'ui' => [ + 'label' => 'Some Label' + ] + ], + 'elementWithUnknownType' => [ + 'type' => 'TypeWithoutDataTypeConfig', + 'ui' => [ + 'label' => 'Some Label', + 'editor' => 'EditorFromPropertyConfig', + ] + ], + 'elementWithEditorFromDataTypeConfig' => [ + 'type' => 'TypeWithDataTypeConfig', + 'ui' => [ + 'editor' => 'EditorFromDataTypeConfig', + 'value' => 'fromPropertyConfig', + 'dataTypeValue' => 'fromDataTypeConfig', + 'elementValue' => 'fromPropertyConfig', + ] + ], + 'elementWithEditorFromDataTypeConfigWithoutUiConfig' => [ + 'type' => 'TypeWithDataTypeConfig', + 'ui' => [ + 'editor' => 'EditorFromDataTypeConfig', + 'value' => 'fromDataTypeConfig', + 'dataTypeValue' => 'fromDataTypeConfig', + ] + ], + 'elementWithOverriddenEditorConfig' => [ + 'type' => 'TypeWithDataTypeConfig', + 'ui' => [ + 'editor' => 'EditorFromPropertyConfig', + 'value' => 'fromPropertyConfig', + 'dataTypeValue' => 'fromDataTypeConfig', + 'elementValue' => 'fromPropertyConfig', + ] + ], + 'elementWithOverriddenEditorConfigAndEditorDefaultConfig' => [ + 'type' => 'TypeWithDataTypeConfig', + 'ui' => [ + 'value' => 'fromPropertyConfig', + 'editorDefaultValue' => 'fromEditorDefaultConfig', + 'editor' => 'EditorWithDefaultConfig', + 'dataTypeValue' => 'fromDataTypeConfig', + 'elementValue' => 'fromPropertyConfig', + ] + ], + 'elementWithEditorDefaultConfig' => [ + 'type' => 'TypeWithDefaultEditorConfig', + 'ui' => [ + 'value' => 'fromPropertyConfig', + 'editorDefaultValue' => 'fromEditorDefaultConfig', + 'editor' => 'EditorWithDefaultConfig', + 'dataTypeValue' => 'fromDataTypeConfig', + 'elementValue' => 'fromPropertyConfig', + ] + ], + 'elementWithOverriddenEditorConfigAndEditorDefaultConfig2' => [ + 'type' => 'TypeWithDefaultEditorConfig', + 'ui' => [ + 'editor' => 'EditorWithoutDefaultConfig', + 'value' => 'fromDataTypeConfig', + 'dataTypeValue' => 'fromDataTypeConfig', + 'elementValue' => 'fromPropertyConfig', + ] + ], + 'elementWithOverriddenEditorConfigAndEditorDefaultConfig3' => [ + 'type' => 'TypeWithDefaultEditorConfig2', + 'ui' => [ + 'value' => 'fromEditorDefaultConfig', + 'editorDefaultValue' => 'fromEditorDefaultConfig', + 'editor' => 'EditorWithDefaultConfig', + 'dataTypeValue' => 'fromDataTypeConfig', + 'elementValue' => 'fromPropertyConfig', + ] + ], + ], + ], + ], + ]; + + $this->postprocessor->process($this->mockNodeType, $configuration, []); + + self::assertSame($expectedResult, $configuration); + } + + /** + * @test + */ + public function processDoesNotThrowExceptionIfNoCreationDialogEditorCanBeResolved(): void + { + $configuration = [ + 'ui' => [ + 'creationDialog' => [ + 'elements' => [ + 'someElement' => [ + 'type' => 'string', + 'ui' => ['label' => 'Foo'] + ], + ], + ], + ], + ]; + $expected = $configuration; + + $this->postprocessor->process($this->mockNodeType, $configuration, []); + + self::assertSame($expected, $configuration); + } +}