diff --git a/Neos.ContentRepository.Core/Classes/EventStore/EventNormalizer.php b/Neos.ContentRepository.Core/Classes/EventStore/EventNormalizer.php index 7b941d86572..b014b810d82 100644 --- a/Neos.ContentRepository.Core/Classes/EventStore/EventNormalizer.php +++ b/Neos.ContentRepository.Core/Classes/EventStore/EventNormalizer.php @@ -32,9 +32,13 @@ use Neos\ContentRepository\Core\Feature\WorkspacePublication\Event\WorkspaceWasPublished; use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Event\WorkspaceRebaseFailed; use Neos\ContentRepository\Core\Feature\WorkspaceRebase\Event\WorkspaceWasRebased; +use Neos\ContentRepository\Core\Feature\WorkspaceModification\Event\WorkspaceWasRenamed; +use Neos\ContentRepository\Core\Feature\WorkspaceModification\Event\WorkspaceWasRemoved; +use Neos\ContentRepository\Core\Feature\WorkspaceModification\Event\WorkspaceOwnerWasChanged; use Neos\EventStore\Model\Event\EventData; use Neos\EventStore\Model\Event; use Neos\EventStore\Model\Event\EventType; +use Neos\ContentRepository\Core\Feature\WorkspaceModification\Event\WorkspaceBaseWorkspaceWasChanged; /** * Central authority to convert Content Repository domain events to Event Store EventData and EventType, vice versa. @@ -85,11 +89,15 @@ public function __construct() RootNodeAggregateDimensionsWereUpdated::class, WorkspaceRebaseFailed::class, WorkspaceWasCreated::class, + WorkspaceWasRenamed::class, WorkspaceWasDiscarded::class, WorkspaceWasPartiallyDiscarded::class, WorkspaceWasPartiallyPublished::class, WorkspaceWasPublished::class, - WorkspaceWasRebased::class + WorkspaceWasRebased::class, + WorkspaceWasRemoved::class, + WorkspaceOwnerWasChanged::class, + WorkspaceBaseWorkspaceWasChanged::class, ]; foreach ($supportedEventClassNames as $fullEventClassName) { diff --git a/Neos.ContentRepository.Core/Classes/Feature/ContentStreamCommandHandler.php b/Neos.ContentRepository.Core/Classes/Feature/ContentStreamCommandHandler.php index ecea56a49dc..51634eac3ba 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/ContentStreamCommandHandler.php +++ b/Neos.ContentRepository.Core/Classes/Feature/ContentStreamCommandHandler.php @@ -100,7 +100,8 @@ private function handleForkContentStream( $sourceContentStreamVersion->unwrap(), ), ), - ExpectedVersion::ANY() + // NO_STREAM to ensure the "fork" happens as the first event of the new content stream + ExpectedVersion::NO_STREAM() ); } diff --git a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php index 182c9a55b07..ae56909cefc 100644 --- a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php +++ b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceCommandHandler.php @@ -31,6 +31,7 @@ use Neos\ContentRepository\Core\Feature\ContentStreamCreation\Command\CreateContentStream; use Neos\ContentRepository\Core\Feature\ContentStreamForking\Command\ForkContentStream; use Neos\ContentRepository\Core\Feature\ContentStreamForking\Event\ContentStreamWasForked; +use Neos\ContentRepository\Core\Feature\ContentStreamRemoval\Command\RemoveContentStream; use Neos\ContentRepository\Core\SharedModel\Exception\ContentStreamAlreadyExists; use Neos\ContentRepository\Core\SharedModel\Exception\ContentStreamDoesNotExistYet; use Neos\ContentRepository\Core\Feature\Common\RebasableToOtherContentStreamsInterface; @@ -51,6 +52,17 @@ use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Exception\BaseWorkspaceDoesNotExist; use Neos\ContentRepository\Core\Feature\WorkspacePublication\Exception\BaseWorkspaceHasBeenModifiedInTheMeantime; use Neos\ContentRepository\Core\Feature\WorkspaceCreation\Exception\WorkspaceAlreadyExists; +use Neos\ContentRepository\Core\Feature\WorkspaceModification\Command\RenameWorkspace; +use Neos\ContentRepository\Core\Feature\WorkspaceModification\Command\DeleteWorkspace; +use Neos\ContentRepository\Core\Feature\WorkspaceModification\Event\WorkspaceWasRemoved; +use Neos\ContentRepository\Core\Feature\WorkspaceModification\Event\WorkspaceWasRenamed; +use Neos\ContentRepository\Core\Feature\WorkspaceModification\Command\ChangeWorkspaceOwner; +use Neos\ContentRepository\Core\Feature\WorkspaceModification\Event\WorkspaceOwnerWasChanged; +use Neos\ContentRepository\Core\Feature\WorkspaceModification\Command\ChangeBaseWorkspace; +use Neos\ContentRepository\Core\Feature\WorkspaceModification\Event\WorkspaceBaseWorkspaceWasChanged; +use Neos\ContentRepository\Core\Feature\WorkspaceModification\Exception\WorkspaceIsNotEmptyException; +use Neos\ContentRepository\Core\Feature\WorkspaceModification\Exception\BaseWorkspaceEqualsWorkspaceException; +use Neos\ContentRepository\Core\Feature\WorkspaceModification\Exception\CircularRelationBetweenWorkspacesException; use Neos\ContentRepository\Core\SharedModel\Exception\WorkspaceDoesNotExist; use Neos\ContentRepository\Core\SharedModel\Exception\WorkspaceHasNoBaseWorkspaceName; use Neos\ContentRepository\Core\Projection\Workspace\Workspace; @@ -61,6 +73,7 @@ use Neos\EventStore\Exception\ConcurrencyException; use Neos\EventStore\Model\EventEnvelope; use Neos\EventStore\Model\EventStream\ExpectedVersion; +use Neos\EventStore\Model\Event\EventType; /** * @internal from userland, you'll use ContentRepository::handle to dispatch commands @@ -84,12 +97,16 @@ public function handle(CommandInterface $command, ContentRepository $contentRepo /** @phpstan-ignore-next-line */ return match ($command::class) { CreateWorkspace::class => $this->handleCreateWorkspace($command, $contentRepository), + RenameWorkspace::class => $this->handleRenameWorkspace($command, $contentRepository), CreateRootWorkspace::class => $this->handleCreateRootWorkspace($command, $contentRepository), PublishWorkspace::class => $this->handlePublishWorkspace($command, $contentRepository), RebaseWorkspace::class => $this->handleRebaseWorkspace($command, $contentRepository), PublishIndividualNodesFromWorkspace::class => $this->handlePublishIndividualNodesFromWorkspace($command, $contentRepository), DiscardIndividualNodesFromWorkspace::class => $this->handleDiscardIndividualNodesFromWorkspace($command, $contentRepository), DiscardWorkspace::class => $this->handleDiscardWorkspace($command, $contentRepository), + DeleteWorkspace::class => $this->handleDeleteWorkspace($command, $contentRepository), + ChangeWorkspaceOwner::class => $this->handleChangeWorkspaceOwner($command, $contentRepository), + ChangeBaseWorkspace::class => $this->handleChangeBaseWorkspace($command, $contentRepository), }; } @@ -146,6 +163,28 @@ private function handleCreateWorkspace( ); } + /** + * @throws WorkspaceDoesNotExist + */ + private function handleRenameWorkspace(RenameWorkspace $command, ContentRepository $contentRepository): EventsToPublish + { + $this->requireWorkspace($command->workspaceName, $contentRepository); + + $events = Events::with( + new WorkspaceWasRenamed( + $command->workspaceName, + $command->workspaceTitle, + $command->workspaceDescription, + ) + ); + + return new EventsToPublish( + WorkspaceEventStreamName::fromWorkspaceName($command->workspaceName)->getEventStreamName(), + $events, + ExpectedVersion::STREAM_EXISTS() + ); + } + /** * @param CreateRootWorkspace $command * @return EventsToPublish @@ -368,8 +407,8 @@ private function handleRebaseWorkspace( $rebaseStatistics->commandRebaseError(sprintf( "The content stream %s cannot be rebased. Error with command %d (%s)" - . " - see nested exception for details.\n\n The base workspace %s is at content stream %s." - . "\n The full list of commands applied so far is: %s", + . " - see nested exception for details.\n\n The base workspace %s is at content stream %s." + . "\n The full list of commands applied so far is: %s", $workspaceContentStreamName->value, $i, get_class($commandToRebase), @@ -693,6 +732,102 @@ private function handleDiscardWorkspace( ); } + /** + * @throws BaseWorkspaceDoesNotExist + * @throws WorkspaceDoesNotExist + * @throws WorkspaceHasNoBaseWorkspaceName + * @throws WorkspaceIsNotEmptyException + * @throws BaseWorkspaceEqualsWorkspaceException + * @throws CircularRelationBetweenWorkspacesException + */ + private function handleChangeBaseWorkspace( + ChangeBaseWorkspace $command, + ContentRepository $contentRepository, + ): EventsToPublish { + $workspace = $this->requireWorkspace($command->workspaceName, $contentRepository); + $this->requireEmptyWorkspace($workspace); + $this->requireBaseWorkspace($workspace, $contentRepository); + + $baseWorkspace = $this->requireWorkspace($command->baseWorkspaceName, $contentRepository); + + $this->requireNonCircularRelationBetweenWorkspaces($workspace, $baseWorkspace, $contentRepository); + + $contentRepository->handle( + new ForkContentStream( + $command->newContentStreamId, + $baseWorkspace->currentContentStreamId, + ) + )->block(); + + $streamName = WorkspaceEventStreamName::fromWorkspaceName($command->workspaceName)->getEventStreamName(); + $events = Events::with( + new WorkspaceBaseWorkspaceWasChanged( + $command->workspaceName, + $command->baseWorkspaceName, + $command->newContentStreamId, + ) + ); + + return new EventsToPublish( + $streamName, + $events, + ExpectedVersion::ANY() + ); + } + + /** + * @throws WorkspaceDoesNotExist + */ + private function handleDeleteWorkspace( + DeleteWorkspace $command, + ContentRepository $contentRepository, + ): EventsToPublish { + $workspace = $this->requireWorkspace($command->workspaceName, $contentRepository); + + $contentRepository->handle( + new RemoveContentStream( + $workspace->currentContentStreamId + ) + )->block(); + + $events = Events::with( + new WorkspaceWasRemoved( + $command->workspaceName, + ) + ); + + $streamName = WorkspaceEventStreamName::fromWorkspaceName($command->workspaceName)->getEventStreamName(); + return new EventsToPublish( + $streamName, + $events, + ExpectedVersion::ANY() + ); + } + + /** + * @throws WorkspaceDoesNotExist + */ + private function handleChangeWorkspaceOwner( + ChangeWorkspaceOwner $command, + ContentRepository $contentRepository, + ): EventsToPublish { + $this->requireWorkspace($command->workspaceName, $contentRepository); + + $events = Events::with( + new WorkspaceOwnerWasChanged( + $command->workspaceName, + $command->newWorkspaceOwner + ) + ); + + $streamName = WorkspaceEventStreamName::fromWorkspaceName($command->workspaceName)->getEventStreamName(); + return new EventsToPublish( + $streamName, + $events, + ExpectedVersion::STREAM_EXISTS() + ); + } + /** * @throws WorkspaceDoesNotExist */ @@ -723,4 +858,57 @@ private function requireBaseWorkspace(Workspace $workspace, ContentRepository $c return $baseWorkspace; } + + /** + * @throws BaseWorkspaceEqualsWorkspaceException + * @throws CircularRelationBetweenWorkspacesException + */ + private function requireNonCircularRelationBetweenWorkspaces(Workspace $workspace, Workspace $baseWorkspace, ContentRepository $contentRepository): void + { + if ($workspace->workspaceName->equals($baseWorkspace->workspaceName)) { + throw new BaseWorkspaceEqualsWorkspaceException(sprintf('The base workspace of the target must be different from the given workspace "%s".', $workspace->workspaceName->value)); + } + + $nextBaseWorkspace = $baseWorkspace; + while ($nextBaseWorkspace?->baseWorkspaceName !== null) { + if ($workspace->workspaceName->equals($nextBaseWorkspace->baseWorkspaceName)) { + throw new CircularRelationBetweenWorkspacesException(sprintf('The workspace "%s" is already on the path of the target workspace "%s".', $workspace->workspaceName->value, $baseWorkspace->workspaceName->value)); + } + $nextBaseWorkspace = $contentRepository->getWorkspaceFinder()->findOneByName($nextBaseWorkspace->baseWorkspaceName); + } + } + + /** + * @throws WorkspaceIsNotEmptyException + */ + private function requireEmptyWorkspace(Workspace $workspace): void + { + $workspaceContentStreamName = ContentStreamEventStreamName::fromContentStreamId( + $workspace->currentContentStreamId + ); + if ($this->hasEventsInContentStreamExceptForking($workspaceContentStreamName)) { + throw new WorkspaceIsNotEmptyException('The user workspace needs to be empty before switching the base workspace.', 1681455989); + } + } + + /** + * @return bool + */ + private function hasEventsInContentStreamExceptForking( + ContentStreamEventStreamName $workspaceContentStreamName, + ): bool { + $workspaceContentStream = $this->eventStore->load($workspaceContentStreamName->getEventStreamName()); + + $fullQualifiedEventClassName = ContentStreamWasForked::class; + $shortEventClassName = substr($fullQualifiedEventClassName, strrpos($fullQualifiedEventClassName, '\\') + 1); + + foreach ($workspaceContentStream as $eventEnvelope) { + if ($eventEnvelope->event->type->value === EventType::fromString($shortEventClassName)->value) { + continue; + } + return true; + } + + return false; + } } diff --git a/Neos.ContentRepository.Core/Classes/Feature/WorkspaceModification/Command/ChangeBaseWorkspace.php b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceModification/Command/ChangeBaseWorkspace.php new file mode 100644 index 00000000000..c20569fb843 --- /dev/null +++ b/Neos.ContentRepository.Core/Classes/Feature/WorkspaceModification/Command/ChangeBaseWorkspace.php @@ -0,0 +1,24 @@ +eventNormalizer->getEventClassName($event); return in_array($eventClassName, [ WorkspaceWasCreated::class, + WorkspaceWasRenamed::class, RootWorkspaceWasCreated::class, WorkspaceWasDiscarded::class, WorkspaceWasPartiallyDiscarded::class, @@ -133,6 +138,9 @@ public function canHandle(Event $event): bool WorkspaceWasPublished::class, WorkspaceWasRebased::class, WorkspaceRebaseFailed::class, + WorkspaceWasRemoved::class, + WorkspaceOwnerWasChanged::class, + WorkspaceBaseWorkspaceWasChanged::class, ]); } @@ -152,6 +160,8 @@ private function apply(EventEnvelope $eventEnvelope): void if ($eventInstance instanceof WorkspaceWasCreated) { $this->whenWorkspaceWasCreated($eventInstance); + } elseif ($eventInstance instanceof WorkspaceWasRenamed) { + $this->whenWorkspaceWasRenamed($eventInstance); } elseif ($eventInstance instanceof RootWorkspaceWasCreated) { $this->whenRootWorkspaceWasCreated($eventInstance); } elseif ($eventInstance instanceof WorkspaceWasDiscarded) { @@ -166,6 +176,12 @@ private function apply(EventEnvelope $eventEnvelope): void $this->whenWorkspaceWasRebased($eventInstance); } elseif ($eventInstance instanceof WorkspaceRebaseFailed) { $this->whenWorkspaceRebaseFailed($eventInstance); + } elseif ($eventInstance instanceof WorkspaceWasRemoved) { + $this->whenWorkspaceWasRemoved($eventInstance); + } elseif ($eventInstance instanceof WorkspaceOwnerWasChanged) { + $this->whenWorkspaceOwnerWasChanged($eventInstance); + } elseif ($eventInstance instanceof WorkspaceBaseWorkspaceWasChanged) { + $this->whenWorkspaceBaseWorkspaceWasChanged($eventInstance); } else { throw new \RuntimeException('Not supported: ' . get_class($eventInstance)); } @@ -206,6 +222,18 @@ private function whenWorkspaceWasCreated(WorkspaceWasCreated $event): void ]); } + private function whenWorkspaceWasRenamed(WorkspaceWasRenamed $event): void + { + $this->getDatabaseConnection()->update( + $this->tableName, + [ + 'workspaceTitle' => $event->workspaceTitle->value, + 'workspaceDescription' => $event->workspaceDescription->value, + ], + ['workspaceName' => $event->workspaceName->value] + ); + } + private function whenRootWorkspaceWasCreated(RootWorkspaceWasCreated $event): void { $this->getDatabaseConnection()->insert($this->tableName, [ @@ -277,9 +305,38 @@ private function whenWorkspaceRebaseFailed(WorkspaceRebaseFailed $event): void $this->markWorkspaceAsOutdatedConflict($event->workspaceName); } + private function whenWorkspaceWasRemoved(WorkspaceWasRemoved $event): void + { + $this->getDatabaseConnection()->delete( + $this->tableName, + ['workspaceName' => $event->workspaceName->value] + ); + } + + private function whenWorkspaceOwnerWasChanged(WorkspaceOwnerWasChanged $event): void + { + $this->getDatabaseConnection()->update( + $this->tableName, + ['workspaceOwner' => $event->newWorkspaceOwner], + ['workspaceName' => $event->workspaceName->value] + ); + } + + private function whenWorkspaceBaseWorkspaceWasChanged(WorkspaceBaseWorkspaceWasChanged $event): void + { + $this->getDatabaseConnection()->update( + $this->tableName, + [ + 'baseWorkspaceName' => $event->baseWorkspaceName->value, + 'currentContentStreamId' => $event->newContentStreamId->value, + ], + ['workspaceName' => $event->workspaceName->value] + ); + } + private function updateContentStreamId( ContentStreamId $contentStreamId, - WorkspaceName $workspaceName + WorkspaceName $workspaceName, ): void { $this->getDatabaseConnection()->update($this->tableName, [ 'currentContentStreamId' => $contentStreamId->value, diff --git a/Neos.ContentRepository.Core/Classes/SharedModel/Workspace/WorkspaceDescription.php b/Neos.ContentRepository.Core/Classes/SharedModel/Workspace/WorkspaceDescription.php index 96ee17b56cc..55f0f7319a7 100644 --- a/Neos.ContentRepository.Core/Classes/SharedModel/Workspace/WorkspaceDescription.php +++ b/Neos.ContentRepository.Core/Classes/SharedModel/Workspace/WorkspaceDescription.php @@ -38,4 +38,9 @@ public function jsonSerialize(): string { return $this->value; } + + public function equals(self $other): bool + { + return $this->value === $other->value; + } } diff --git a/Neos.ContentRepositoryRegistry/Classes/Command/ContentStreamCommandController.php b/Neos.ContentRepositoryRegistry/Classes/Command/ContentStreamCommandController.php index 964de307bb7..88499afa296 100644 --- a/Neos.ContentRepositoryRegistry/Classes/Command/ContentStreamCommandController.php +++ b/Neos.ContentRepositoryRegistry/Classes/Command/ContentStreamCommandController.php @@ -133,7 +133,7 @@ public function pruneCommand(string $contentRepositoryIdentifier = 'default', bo $unusedContentStreams = $contentStreamPruner->prune($removeTemporary); $unusedContentStreamsPresent = false; foreach ($unusedContentStreams as $contentStream) { - $this->outputFormatted('Removed %s', [$contentStream]); + $this->outputFormatted('Removed %s', [$contentStream->value]); $unusedContentStreamsPresent = true; } if (!$unusedContentStreamsPresent) { @@ -152,7 +152,7 @@ public function pruneRemovedFromEventStreamCommand(string $contentRepositoryIden $unusedContentStreams = $contentStreamPruner->pruneRemovedFromEventStream(); $unusedContentStreamsPresent = false; foreach ($unusedContentStreams as $contentStream) { - $this->outputFormatted('Removed events for %s', [$contentStream]); + $this->outputFormatted('Removed events for %s', [$contentStream->value]); $unusedContentStreamsPresent = true; } if (!$unusedContentStreamsPresent) { diff --git a/Neos.Neos/Classes/Controller/Module/Management/WorkspacesController.php b/Neos.Neos/Classes/Controller/Module/Management/WorkspacesController.php index d08c3ea6667..eabb52d7226 100644 --- a/Neos.Neos/Classes/Controller/Module/Management/WorkspacesController.php +++ b/Neos.Neos/Classes/Controller/Module/Management/WorkspacesController.php @@ -29,6 +29,9 @@ use Neos\ContentRepository\Core\Feature\WorkspacePublication\Command\DiscardWorkspace; use Neos\ContentRepository\Core\Feature\WorkspacePublication\Command\PublishIndividualNodesFromWorkspace; use Neos\ContentRepository\Core\Feature\WorkspacePublication\Command\PublishWorkspace; +use Neos\ContentRepository\Core\Feature\WorkspaceModification\Command\RenameWorkspace; +use Neos\ContentRepository\Core\Feature\WorkspaceModification\Command\DeleteWorkspace; +use Neos\ContentRepository\Core\Feature\WorkspaceModification\Command\ChangeWorkspaceOwner; use Neos\Neos\FrontendRouting\NodeAddress; use Neos\Neos\FrontendRouting\NodeAddressFactory; use Neos\ContentRepository\Core\SharedModel\User\UserId; @@ -48,7 +51,6 @@ use Neos\Flow\Mvc\ActionRequest; use Neos\Flow\Package\PackageManager; use Neos\Flow\Property\PropertyMapper; -use Neos\Flow\Property\TypeConverter\PersistentObjectConverter; use Neos\Flow\Security\Context; use Neos\Media\Domain\Model\AssetInterface; use Neos\Media\Domain\Model\ImageInterface; @@ -213,7 +215,7 @@ public function createAction( WorkspaceTitle $title, WorkspaceName $baseWorkspace, string $visibility, - WorkspaceDescription $description + WorkspaceDescription $description, ) { $contentRepositoryId = SiteDetectionResult::fromRequest($this->request->getHttpRequest()) ->contentRepositoryId; @@ -221,12 +223,12 @@ public function createAction( $workspaceName = WorkspaceName::fromString( Utility::renderValidNodeName($title->value) . '-' - . substr(base_convert(microtime(false), 10, 36), -5, 5) + . substr(base_convert(microtime(false), 10, 36), -5, 5) ); while ($contentRepository->getWorkspaceFinder()->findOneByName($workspaceName) instanceof Workspace) { $workspaceName = WorkspaceName::fromString( Utility::renderValidNodeName($title->value) . '-' - . substr(base_convert(microtime(false), 10, 36), -5, 5) + . substr(base_convert(microtime(false), 10, 36), -5, 5) ); } @@ -261,16 +263,16 @@ public function createAction( /** * Edit a workspace * - * @param WorkspaceName $workspace + * @param WorkspaceName $workspaceName * @return void */ - public function editAction(WorkspaceName $workspace) + public function editAction(WorkspaceName $workspaceName) { $contentRepositoryId = SiteDetectionResult::fromRequest($this->request->getHttpRequest()) ->contentRepositoryId; $contentRepository = $this->contentRepositoryRegistry->get($contentRepositoryId); - $workspace = $contentRepository->getWorkspaceFinder()->findOneByName($workspace); + $workspace = $contentRepository->getWorkspaceFinder()->findOneByName($workspaceName); if (is_null($workspace)) { // @todo add flash message $this->redirect('index'); @@ -287,38 +289,59 @@ public function editAction(WorkspaceName $workspace) $this->view->assign('ownerOptions', $this->prepareOwnerOptions()); } - /** - * @return void - */ - protected function initializeUpdateAction() - { - $converter = new PersistentObjectConverter(); - $this->arguments->getArgument('workspace')->getPropertyMappingConfiguration() - ->forProperty('owner') - ->setTypeConverter($converter) - ->setTypeConverterOption( - PersistentObjectConverter::class, - PersistentObjectConverter::CONFIGURATION_TARGET_TYPE, - User::class - ); - parent::initializeAction(); - } - /** * Update a workspace * - * @param Workspace $workspace A workspace to update + * @Flow\Validate(argumentName="title", type="\Neos\Flow\Validation\Validator\NotEmptyValidator") + * @param WorkspaceName $workspaceName + * @param WorkspaceTitle $title Human friendly title of the workspace, for example "Christmas Campaign" + * @param WorkspaceDescription $description A description explaining the purpose of the new workspace + * @param string $workspaceOwner Id of the owner of the workspace * @return void */ - public function updateAction(Workspace $workspace) + public function updateAction(WorkspaceName $workspaceName, WorkspaceTitle $title, WorkspaceDescription $description, ?string $workspaceOwner) { - #if ($workspace->getTitle() === '') { - # $workspace->setTitle($workspace->getName()); - #} - #$this->workspaceFinder->update($workspace); + $contentRepositoryId = SiteDetectionResult::fromRequest($this->request->getHttpRequest()) + ->contentRepositoryId; + $contentRepository = $this->contentRepositoryRegistry->get($contentRepositoryId); + + if ($title->value === '') { + $title = WorkspaceTitle::fromString($workspaceName->value); + } + + $workspace = $contentRepository->getWorkspaceFinder()->findOneByName($workspaceName); + if ($workspace === null) { + $this->addFlashMessage( + $this->getModuleLabel('workspaces.workspaceDoesNotExist'), + '', + Message::SEVERITY_ERROR + ); + $this->redirect('index'); + return; + } + + if (!$workspace->workspaceTitle?->equals($title) || !$workspace->workspaceDescription->equals($description)) { + $contentRepository->handle( + new RenameWorkspace( + $workspaceName, + $title, + $description + ) + )->block(); + } + + if ($workspace->workspaceOwner !== $workspaceOwner) { + $contentRepository->handle( + new ChangeWorkspaceOwner( + $workspaceName, + $workspaceOwner ?: null, + ) + )->block(); + } + $this->addFlashMessage($this->translator->translateById( 'workspaces.workspaceHasBeenUpdated', - [$workspace->workspaceTitle?->value], + [$title->value], null, null, 'Modules', @@ -330,17 +353,29 @@ public function updateAction(Workspace $workspace) /** * Delete a workspace * - * @param Workspace $workspace A workspace to delete + * @param WorkspaceName $workspaceName A workspace to delete * @return void */ - public function deleteAction(Workspace $workspace) + public function deleteAction(WorkspaceName $workspaceName) { $contentRepositoryId = SiteDetectionResult::fromRequest($this->request->getHttpRequest()) ->contentRepositoryId; $contentRepository = $this->contentRepositoryRegistry->get($contentRepositoryId); + $workspace = $contentRepository->getWorkspaceFinder()->findOneByName($workspaceName); + if ($workspace === null) { + $this->addFlashMessage( + $this->getModuleLabel('workspaces.workspaceDoesNotExist'), + '', + Message::SEVERITY_ERROR + ); + $this->redirect('index'); + return; + } + if ($workspace->isPersonalWorkspace()) { $this->redirect('index'); + return; } $dependentWorkspaces = $contentRepository->getWorkspaceFinder() @@ -365,35 +400,43 @@ public function deleteAction(Workspace $workspace) } $nodesCount = 0; - /** @todo something else + try { - $nodesCount = $this->publishingService->getUnpublishedNodesCount($workspace); + $nodesCount = $contentRepository->projectionState(ChangeFinder::class) + ->countByContentStreamId( + $workspace->currentContentStreamId + ); } catch (\Exception $exception) { - $message = $this->translator->translateById( - 'workspaces.notDeletedErrorWhileFetchingUnpublishedNodes', - [(string)$workspace->$this->workspaceTitle], - null, - null, - 'Modules', - 'Neos.Neos' - ) ?: 'workspaces.notDeletedErrorWhileFetchingUnpublishedNodes'; - $this->addFlashMessage($message, '', Message::SEVERITY_WARNING); - $this->redirect('index'); - }*/ - //if ($nodesCount > 0) { - $message = $this->translator->translateById( - 'workspaces.workspaceCannotBeDeletedBecauseOfUnpublishedNodes', - [$workspace->workspaceTitle?->value, $nodesCount], - $nodesCount, - null, - 'Modules', - 'Neos.Neos' - ) ?: 'workspaces.workspaceCannotBeDeletedBecauseOfUnpublishedNodes'; - $this->addFlashMessage($message, '', Message::SEVERITY_WARNING); - $this->redirect('index'); - //} + $message = $this->translator->translateById( + 'workspaces.notDeletedErrorWhileFetchingUnpublishedNodes', + [$workspace->workspaceTitle?->value], + null, + null, + 'Modules', + 'Neos.Neos' + ) ?: 'workspaces.notDeletedErrorWhileFetchingUnpublishedNodes'; + $this->addFlashMessage($message, '', Message::SEVERITY_WARNING); + $this->redirect('index'); + } + if ($nodesCount > 0) { + $message = $this->translator->translateById( + 'workspaces.workspaceCannotBeDeletedBecauseOfUnpublishedNodes', + [$workspace->workspaceTitle?->value, $nodesCount], + $nodesCount, + null, + 'Modules', + 'Neos.Neos' + ) ?: 'workspaces.workspaceCannotBeDeletedBecauseOfUnpublishedNodes'; + $this->addFlashMessage($message, '', Message::SEVERITY_WARNING); + $this->redirect('index'); + } + + $contentRepository->handle( + new DeleteWorkspace( + $workspaceName, + ) + )->block(); - //$this->workspaceFinder->remove($workspace); $this->addFlashMessage($this->translator->translateById( 'workspaces.workspaceHasBeenRemoved', [$workspace->workspaceTitle?->value], @@ -422,22 +465,22 @@ public function rebaseAndRedirectAction(Node $targetNode, Workspace $targetWorks /** @var Workspace $personalWorkspace */ /** @todo do something else - if ($personalWorkspace !== $targetWorkspace) { - if ($this->publishingService->getUnpublishedNodesCount($personalWorkspace) > 0) { - $message = $this->translator->translateById( - 'workspaces.cantEditBecauseWorkspaceContainsChanges', - [], - null, - null, - 'Modules', - 'Neos.Neos' - ) ?: 'workspaces.cantEditBecauseWorkspaceContainsChanges'; - $this->addFlashMessage($message, '', Message::SEVERITY_WARNING, [], 1437833387); - $this->redirect('show', null, null, ['workspace' => $targetWorkspace]); - } - $personalWorkspace->setBaseWorkspace($targetWorkspace); - $this->workspaceFinder->update($personalWorkspace); - } + * if ($personalWorkspace !== $targetWorkspace) { + * if ($this->publishingService->getUnpublishedNodesCount($personalWorkspace) > 0) { + * $message = $this->translator->translateById( + * 'workspaces.cantEditBecauseWorkspaceContainsChanges', + * [], + * null, + * null, + * 'Modules', + * 'Neos.Neos' + * ) ?: 'workspaces.cantEditBecauseWorkspaceContainsChanges'; + * $this->addFlashMessage($message, '', Message::SEVERITY_WARNING, [], 1437833387); + * $this->redirect('show', null, null, ['workspace' => $targetWorkspace]); + * } + * $personalWorkspace->setBaseWorkspace($targetWorkspace); + * $this->workspaceFinder->update($personalWorkspace); + * } */ $targetNodeAddressInPersonalWorkspace = new NodeAddress( @@ -817,7 +860,7 @@ protected function computeSiteChanges(Workspace $selectedWorkspace, ContentRepos protected function getOriginalNode( Node $modifiedNode, ContentStreamId $baseContentStreamId, - ContentRepository $contentRepository + ContentRepository $contentRepository, ): ?Node { $baseSubgraph = $contentRepository->getContentGraph()->getSubgraph( $baseContentStreamId, @@ -838,7 +881,7 @@ protected function getOriginalNode( protected function renderContentChanges( Node $changedNode, ContentStreamId $contentStreamIdOfOriginalNode, - ContentRepository $contentRepository + ContentRepository $contentRepository, ): array { $currentWorkspace = $contentRepository->getWorkspaceFinder()->findOneByCurrentContentStreamId( $contentStreamIdOfOriginalNode @@ -1021,7 +1064,7 @@ protected function postProcessDiffArray(array &$diffArray): void */ protected function prepareBaseWorkspaceOptions( ContentRepository $contentRepository, - Workspace $excludedWorkspace = null + Workspace $excludedWorkspace = null, ): array { $baseWorkspaceOptions = []; $workspaces = $contentRepository->getWorkspaceFinder()->findAll(); @@ -1061,7 +1104,7 @@ protected function prepareOwnerOptions(): array private function getBaseWorkspaceWhenSureItExists( Workspace $workspace, - ContentRepository $contentRepository + ContentRepository $contentRepository, ): Workspace { /** @var WorkspaceName $baseWorkspaceName We expect this to exist */ $baseWorkspaceName = $workspace->baseWorkspaceName; diff --git a/Neos.Neos/Resources/Private/Templates/Module/Management/Workspaces/Edit.html b/Neos.Neos/Resources/Private/Templates/Module/Management/Workspaces/Edit.html index 7cb613248dd..5e7b7e0bc91 100644 --- a/Neos.Neos/Resources/Private/Templates/Module/Management/Workspaces/Edit.html +++ b/Neos.Neos/Resources/Private/Templates/Module/Management/Workspaces/Edit.html @@ -2,38 +2,31 @@ -

{neos:backend.translate(id: 'workspaces.editWorkspace', source: 'Modules', package: 'Neos.Neos', arguments: {0: workspace.title})}

+

{neos:backend.translate(id: 'workspaces.editWorkspace', source: 'Modules', package: 'Neos.Neos', arguments: {0: workspace.workspaceTitle.value})}

- + +
- +
- +
- - - - - - - - - +
@@ -41,7 +34,7 @@

{neos:backend.translate(id: 'workspaces.editWorkspace', source: 'Modules', p
- +
diff --git a/Neos.Neos/Resources/Private/Templates/Module/Management/Workspaces/Index.html b/Neos.Neos/Resources/Private/Templates/Module/Management/Workspaces/Index.html index c1aee362a04..3debac241b7 100644 --- a/Neos.Neos/Resources/Private/Templates/Module/Management/Workspaces/Index.html +++ b/Neos.Neos/Resources/Private/Templates/Module/Management/Workspaces/Index.html @@ -89,7 +89,7 @@
- + @@ -127,7 +127,7 @@