diff --git a/composer.json b/composer.json index 014c6d30c6..3b5ab4bcc5 100644 --- a/composer.json +++ b/composer.json @@ -53,6 +53,7 @@ "ibexa/user": "~4.6.0@dev", "ibexa/fieldtype-richtext": "~4.6.0@dev", "ibexa/rest": "~4.6.0@dev", + "ibexa/phpstan": "~4.6.x-dev", "ibexa/polyfill-php82": "^1.0", "ibexa/search": "~4.6.x-dev", "ibexa/twig-components": "~4.6.x-dev", diff --git a/phpstan.neon b/phpstan.neon index 51003c467b..7d75fe9f96 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -3,6 +3,7 @@ includes: - phpstan-baseline.neon.php - vendor/phpstan/phpstan-phpunit/extension.neon - vendor/phpstan/phpstan-symfony/extension.neon + - vendor/ibexa/phpstan/extension.neon parameters: level: 8 diff --git a/src/lib/Menu/ContentRightSidebarBuilder.php b/src/lib/Menu/ContentRightSidebarBuilder.php index 84d5085c5b..64fe897c9e 100644 --- a/src/lib/Menu/ContentRightSidebarBuilder.php +++ b/src/lib/Menu/ContentRightSidebarBuilder.php @@ -12,6 +12,7 @@ use Ibexa\AdminUi\Siteaccess\SiteaccessResolverInterface; use Ibexa\AdminUi\Specification\ContentType\ContentTypeIsUser; use Ibexa\AdminUi\Specification\ContentType\ContentTypeIsUserGroup; +use Ibexa\AdminUi\Specification\Location\IsInContextualTreeRootIds; use Ibexa\AdminUi\Specification\Location\IsRoot; use Ibexa\AdminUi\Specification\Location\IsWithinCopySubtreeLimit; use Ibexa\AdminUi\UniversalDiscovery\ConfigResolver; @@ -293,7 +294,10 @@ public function createStructure(array $options): ItemInterface ); } - if (!$contentIsUser && 1 !== $location->depth && $canTrashLocation) { + $isAtRootLevel = (new IsRoot())->isSatisfiedBy($location); + $isInContextualRootIds = (new IsInContextualTreeRootIds($this->configResolver))->isSatisfiedBy($location); + + if (!$contentIsUser && !$isAtRootLevel && !$isInContextualRootIds && $canTrashLocation) { $menu->addChild( $this->createMenuItem( self::ITEM__SEND_TO_TRASH, @@ -305,7 +309,7 @@ public function createStructure(array $options): ItemInterface ); } - if (1 === $location->depth) { + if ($isAtRootLevel || $isInContextualRootIds) { $menu[self::ITEM__MOVE]->setAttribute('disabled', 'disabled'); } diff --git a/src/lib/Specification/Location/IsContentStructureRoot.php b/src/lib/Specification/Location/IsContentStructureRoot.php new file mode 100644 index 0000000000..acda399d6b --- /dev/null +++ b/src/lib/Specification/Location/IsContentStructureRoot.php @@ -0,0 +1,30 @@ +configResolver = $configResolver; + } + + /** + * @param \Ibexa\Contracts\Core\Repository\Values\Content\Location $item + */ + public function isSatisfiedBy($item): bool + { + return $item->getId() === (int)$this->configResolver->getParameter('location_ids.content_structure'); + } +} diff --git a/src/lib/Specification/Location/IsInContextualTreeRootIds.php b/src/lib/Specification/Location/IsInContextualTreeRootIds.php new file mode 100644 index 0000000000..e6c19c714b --- /dev/null +++ b/src/lib/Specification/Location/IsInContextualTreeRootIds.php @@ -0,0 +1,34 @@ +configResolver = $configResolver; + } + + /** + * @param \Ibexa\Contracts\Core\Repository\Values\Content\Location $item + */ + public function isSatisfiedBy($item): bool + { + $contextualRootIds = $this->configResolver->getParameter( + 'content_tree_module.contextual_tree_root_location_ids' + ); + + return in_array($item->getId(), $contextualRootIds, true); + } +} diff --git a/src/lib/Specification/Location/IsRoot.php b/src/lib/Specification/Location/IsRoot.php index bb57ed4327..25f46882cd 100644 --- a/src/lib/Specification/Location/IsRoot.php +++ b/src/lib/Specification/Location/IsRoot.php @@ -14,12 +14,10 @@ class IsRoot extends AbstractSpecification { /** * @param \Ibexa\Contracts\Core\Repository\Values\Content\Location $item - * - * @return bool */ public function isSatisfiedBy($item): bool { - return 1 === $item->depth; + return 1 === $item->getDepth(); } } diff --git a/tests/lib/Specification/Location/IsContentStructureRootTest.php b/tests/lib/Specification/Location/IsContentStructureRootTest.php new file mode 100644 index 0000000000..881e7556f8 --- /dev/null +++ b/tests/lib/Specification/Location/IsContentStructureRootTest.php @@ -0,0 +1,66 @@ +createConfigResolverReturning($id) + ); + + self::assertTrue( + $specification->isSatisfiedBy($this->createLocationWithId($id)) + ); + } + + /** + * @covers \Ibexa\AdminUi\Specification\Location\IsContentStructureRoot::isSatisfiedBy + */ + public function testReturnsFalseWhenLocationDepthDoesNotMatchRoot(): void + { + $specification = new IsContentStructureRoot( + $this->createConfigResolverReturning(1) + ); + + self::assertFalse( + $specification->isSatisfiedBy($this->createLocationWithId(3)) + ); + } + + private function createLocationWithId(int $id): Location + { + $location = $this->createMock(Location::class); + $location->method('getId')->willReturn($id); + + return $location; + } + + private function createConfigResolverReturning(int $id): ConfigResolverInterface + { + $configResolver = $this->createMock(ConfigResolverInterface::class); + $configResolver + ->method('getParameter') + ->with('location_ids.content_structure') + ->willReturn($id); + + return $configResolver; + } +} diff --git a/tests/lib/Specification/Location/IsInContextualTreeRootIdsTest.php b/tests/lib/Specification/Location/IsInContextualTreeRootIdsTest.php new file mode 100644 index 0000000000..cdbb985bd9 --- /dev/null +++ b/tests/lib/Specification/Location/IsInContextualTreeRootIdsTest.php @@ -0,0 +1,66 @@ +createConfigResolverReturning() + ); + + self::assertTrue( + $specification->isSatisfiedBy($this->createLocationWithId(43)) + ); + } + + /** + * @covers \Ibexa\AdminUi\Specification\Location\IsInContextualTreeRootIds::isSatisfiedBy + */ + public function testReturnsFalseWhenLocationIdIsNotInContextualRootList(): void + { + $specification = new IsInContextualTreeRootIds( + $this->createConfigResolverReturning() + ); + + self::assertFalse( + $specification->isSatisfiedBy($this->createLocationWithId(999)) + ); + } + + private function createLocationWithId(int $id): Location + { + $location = $this->createMock(Location::class); + $location->method('getId')->willReturn($id); + + return $location; + } + + private function createConfigResolverReturning(): ConfigResolverInterface + { + $configResolver = $this->createMock(ConfigResolverInterface::class); + $configResolver + ->method('getParameter') + ->with('content_tree_module.contextual_tree_root_location_ids') + ->willReturn(self::CONTEXTUAL_ROOT_IDS); + + return $configResolver; + } +} diff --git a/tests/lib/Specification/Location/IsRootTest.php b/tests/lib/Specification/Location/IsRootTest.php new file mode 100644 index 0000000000..06cb86966a --- /dev/null +++ b/tests/lib/Specification/Location/IsRootTest.php @@ -0,0 +1,48 @@ +createLocationWithDepth(1); + + self::assertTrue($specification->isSatisfiedBy($location)); + } + + /** + * @covers \Ibexa\AdminUi\Specification\Location\IsRoot::isSatisfiedBy + */ + public function testReturnsFalseWhenLocationDepthIsNotOne(): void + { + $specification = new IsRoot(); + + $location = $this->createLocationWithDepth(2); + + self::assertFalse($specification->isSatisfiedBy($location)); + } + + private function createLocationWithDepth(int $depth): Location + { + $location = $this->createMock(Location::class); + $location->method('getDepth')->willReturn($depth); + + return $location; + } +} diff --git a/tests/lib/Validator/Constraint/LocationIsNotRootValidatorTest.php b/tests/lib/Validator/Constraint/LocationIsNotRootValidatorTest.php index 250884b6bd..b5df19d147 100644 --- a/tests/lib/Validator/Constraint/LocationIsNotRootValidatorTest.php +++ b/tests/lib/Validator/Constraint/LocationIsNotRootValidatorTest.php @@ -31,11 +31,8 @@ protected function setUp(): void public function testValid() { - $location = $this - ->getMockBuilder(Location::class) - ->setMethodsExcept(['__get']) - ->setConstructorArgs([['depth' => 5]]) - ->getMock(); + $location = $this->createMock(Location::class); + $location->method('getDepth')->willReturn(5); $this->executionContext ->expects($this->never()) @@ -46,11 +43,8 @@ public function testValid() public function testInvalid() { - $location = $this - ->getMockBuilder(Location::class) - ->setMethodsExcept(['__get']) - ->setConstructorArgs([['depth' => 1]]) - ->getMock(); + $location = $this->createMock(Location::class); + $location->method('getDepth')->willReturn(1); $this->executionContext ->expects($this->once())