diff --git a/src/bundle/Controller/LocationController.php b/src/bundle/Controller/LocationController.php index 3fc3fb7ace..427ba86eb9 100644 --- a/src/bundle/Controller/LocationController.php +++ b/src/bundle/Controller/LocationController.php @@ -255,7 +255,7 @@ public function swapAction(Request $request): Response $currentLocation = $data->getCurrentLocation(); $newLocation = $data->getNewLocation(); - $childCount = $this->locationService->getLocationChildCount($currentLocation); + $childCount = $this->locationService->getLocationChildCount($currentLocation, 1); $contentType = $newLocation?->getContent()->getContentType(); if (!$contentType?->isContainer() && $childCount) { diff --git a/src/bundle/DependencyInjection/Configuration/Parser/SubtreeOperations.php b/src/bundle/DependencyInjection/Configuration/Parser/SubtreeOperations.php index 7856d5752a..550f53d7c6 100644 --- a/src/bundle/DependencyInjection/Configuration/Parser/SubtreeOperations.php +++ b/src/bundle/DependencyInjection/Configuration/Parser/SubtreeOperations.php @@ -23,6 +23,8 @@ * subtree_operations: * copy_subtree: * limit: 200 + * query_subtree: + * limit: 500 * ``` */ final class SubtreeOperations extends AbstractParser @@ -35,15 +37,25 @@ public function mapConfig( mixed $currentScope, ContextualizerInterface $contextualizer ): void { - if (!isset($scopeSettings['subtree_operations']['copy_subtree']['limit'])) { + if (!isset($scopeSettings['subtree_operations'])) { return; } - $contextualizer->setContextualParameter( - 'subtree_operations.copy_subtree.limit', - $currentScope, - $scopeSettings['subtree_operations']['copy_subtree']['limit'] - ); + if (isset($scopeSettings['subtree_operations']['copy_subtree']['limit'])) { + $contextualizer->setContextualParameter( + 'subtree_operations.copy_subtree.limit', + $currentScope, + $scopeSettings['subtree_operations']['copy_subtree']['limit'] + ); + } + + if (isset($scopeSettings['subtree_operations']['query_subtree']['limit'])) { + $contextualizer->setContextualParameter( + 'subtree_operations.query_subtree.limit', + $currentScope, + $scopeSettings['subtree_operations']['query_subtree']['limit'] + ); + } } public function addSemanticConfig(NodeBuilder $nodeBuilder): void @@ -60,6 +72,12 @@ public function addSemanticConfig(NodeBuilder $nodeBuilder): void ->end() ->end() ->end() + ->arrayNode('query_subtree') + ->children() + ->integerNode('limit') + ->info('Limit the total count of items queried for when calculating the number of direct children a node has. -1 for no limit.') + ->end() + ->end() ->end() ->end(); } diff --git a/src/bundle/Resources/config/ezplatform_default_settings.yaml b/src/bundle/Resources/config/ezplatform_default_settings.yaml index 15bcba7a60..7babb1ba12 100644 --- a/src/bundle/Resources/config/ezplatform_default_settings.yaml +++ b/src/bundle/Resources/config/ezplatform_default_settings.yaml @@ -42,6 +42,7 @@ parameters: # Subtree Operations ibexa.site_access.config.admin_group.subtree_operations.copy_subtree.limit: 100 + ibexa.site_access.config.admin_group.subtree_operations.query_subtree.limit: 500 # Notifications ibexa.site_access.config.admin_group.notification_count.interval: 30000 diff --git a/src/bundle/Resources/views/themes/admin/content/tab/locations/tab.html.twig b/src/bundle/Resources/views/themes/admin/content/tab/locations/tab.html.twig index 72062f6e92..0fa5775eeb 100644 --- a/src/bundle/Resources/views/themes/admin/content/tab/locations/tab.html.twig +++ b/src/bundle/Resources/views/themes/admin/content/tab/locations/tab.html.twig @@ -82,7 +82,7 @@ }]) %} {% set body_row_cols = body_row_cols|merge([ - { content: location.childCount }, + { content: location.childCount is null ? (sub_item_query_limit ~ '+') : location.childCount }, ]) %} {% set body_rows = body_rows|merge([{ cols: body_row_cols }]) %} diff --git a/src/lib/Form/TrashLocationOptionProvider/HasChildren.php b/src/lib/Form/TrashLocationOptionProvider/HasChildren.php index 89231ae576..fa753d00ee 100644 --- a/src/lib/Form/TrashLocationOptionProvider/HasChildren.php +++ b/src/lib/Form/TrashLocationOptionProvider/HasChildren.php @@ -11,6 +11,7 @@ use Ibexa\AdminUi\Specification\Location\HasChildren as HasChildrenSpec; use Ibexa\Contracts\Core\Repository\LocationService; use Ibexa\Contracts\Core\Repository\Values\Content\Location; +use Ibexa\Contracts\Core\SiteAccess\ConfigResolverInterface; use JMS\TranslationBundle\Annotation\Desc; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\FormInterface; @@ -20,7 +21,8 @@ { public function __construct( private LocationService $locationService, - private TranslatorInterface $translator + private TranslatorInterface $translator, + private ConfigResolverInterface $configResolver ) { } @@ -31,10 +33,15 @@ public function supports(Location $location): bool public function addOptions(FormInterface $form, Location $location): void { - $childCount = $this->locationService->getLocationChildCount($location); + $limit = (int) $this->configResolver->getParameter('subtree_operations.query_subtree.limit'); + + $useLimit = $limit > 0; + $childCount = $this->locationService->getLocationChildCount($location, $useLimit ? $limit + 1 : null); $translatorParameters = [ - '%children_count%' => $childCount, + '%children_count%' => ($useLimit && $childCount >= $limit) ? + sprintf('%d+', $limit) : + $childCount, '%content%' => $location->getContent()->getName(), ]; diff --git a/src/lib/Specification/Location/HasChildren.php b/src/lib/Specification/Location/HasChildren.php index c1b5afb9c5..185cb3d73b 100644 --- a/src/lib/Specification/Location/HasChildren.php +++ b/src/lib/Specification/Location/HasChildren.php @@ -22,7 +22,7 @@ public function __construct(private readonly LocationService $locationService) */ public function isSatisfiedBy(mixed $item): bool { - $childCount = $this->locationService->getLocationChildCount($item); + $childCount = $this->locationService->getLocationChildCount($item, 1); return 0 < $childCount; } diff --git a/src/lib/Specification/Location/IsWithinCopySubtreeLimit.php b/src/lib/Specification/Location/IsWithinCopySubtreeLimit.php index f56cdda1c3..78a947997a 100644 --- a/src/lib/Specification/Location/IsWithinCopySubtreeLimit.php +++ b/src/lib/Specification/Location/IsWithinCopySubtreeLimit.php @@ -36,7 +36,7 @@ public function isSatisfiedBy(mixed $item): bool return false; } - return $this->copyLimit >= $this->locationService->getSubtreeSize($item); + return $this->copyLimit >= $this->locationService->getSubtreeSize($item, $this->copyLimit + 1); } private function isContainer(Location $location): bool diff --git a/src/lib/Tab/LocationView/LocationsTab.php b/src/lib/Tab/LocationView/LocationsTab.php index 49012f0fac..8baff3bb24 100644 --- a/src/lib/Tab/LocationView/LocationsTab.php +++ b/src/lib/Tab/LocationView/LocationsTab.php @@ -153,12 +153,18 @@ public function getTemplateParameters(array $contextParameters = []): array ); } + $subItemQueryLimit = $this->configResolver->getParameter('subtree_operations.query_subtree.limit'); + if ($subItemQueryLimit <= 0) { + $subItemQueryLimit = null; + } + $viewParameters = [ 'pager' => $pagination, 'pager_options' => [ 'pageParameter' => sprintf('[%s]', self::PAGINATION_PARAM_NAME), ], 'locations' => $locations, + 'sub_item_query_limit' => $subItemQueryLimit, 'form_content_location_add' => $formLocationAdd->createView(), 'form_content_location_remove' => $formLocationRemove->createView(), 'form_content_location_swap' => $formLocationSwap->createView(), diff --git a/src/lib/UI/Module/Subitems/ContentViewParameterSupplier.php b/src/lib/UI/Module/Subitems/ContentViewParameterSupplier.php index e20c4cfe4e..0edc779ba0 100644 --- a/src/lib/UI/Module/Subitems/ContentViewParameterSupplier.php +++ b/src/lib/UI/Module/Subitems/ContentViewParameterSupplier.php @@ -125,7 +125,12 @@ private function createRestLocation(Location $location): RestLocation { return new RestLocation( $location, - $this->locationService->getLocationChildCount($location) + $this->locationService->getLocationChildCount( + $location, + // For the sub items module we only ever use the count to determine if there are children (0 or 1+), + // hence setting a limit of 1 is sufficient here. + 1 + ) ); } diff --git a/src/lib/UI/Value/Content/Location.php b/src/lib/UI/Value/Content/Location.php index 9ff045c366..27ca4c8aef 100644 --- a/src/lib/UI/Value/Content/Location.php +++ b/src/lib/UI/Value/Content/Location.php @@ -17,7 +17,7 @@ */ class Location extends CoreLocation { - protected int $childCount; + protected ?int $childCount; protected bool $main; diff --git a/src/lib/UI/Value/ValueFactory.php b/src/lib/UI/Value/ValueFactory.php index 7dc529b799..c54be25dcc 100644 --- a/src/lib/UI/Value/ValueFactory.php +++ b/src/lib/UI/Value/ValueFactory.php @@ -36,6 +36,7 @@ use Ibexa\Contracts\Core\Repository\Values\ObjectState\ObjectStateGroup; use Ibexa\Contracts\Core\Repository\Values\User\Policy; use Ibexa\Contracts\Core\Repository\Values\User\RoleAssignment; +use Ibexa\Contracts\Core\SiteAccess\ConfigResolverInterface; use Ibexa\Core\MVC\Symfony\Locale\UserLanguagePreferenceProviderInterface; use Ibexa\Core\Repository\LocationResolver\LocationResolver; use RuntimeException; @@ -53,7 +54,8 @@ public function __construct( protected PathService $pathService, protected DatasetFactory $datasetFactory, private UserLanguagePreferenceProviderInterface $userLanguagePreferenceProvider, - protected LocationResolver $locationResolver + protected LocationResolver $locationResolver, + protected ConfigResolverInterface $configResolver, ) { } @@ -137,9 +139,12 @@ public function createLocation(Location $location): UIValue\Content\Location { $translations = $location->getContent()->getVersionInfo()->getLanguageCodes(); $target = (new Target\Version())->deleteTranslations($translations); + $limit = $this->configResolver->getParameter('subtree_operations.query_subtree.limit'); + $useLimit = $limit > 0; + $count = $this->locationService->getLocationChildCount($location, $useLimit ? $limit : null); return new UIValue\Content\Location($location, [ - 'childCount' => $this->locationService->getLocationChildCount($location), + 'childCount' => $useLimit && $count >= $limit ? null : $count, 'pathLocations' => $this->pathService->loadPathLocations($location), 'userCanManage' => $this->permissionResolver->canUser( 'content', diff --git a/tests/bundle/DependencyInjection/Configuration/Parser/SubtreeOperationsTest.php b/tests/bundle/DependencyInjection/Configuration/Parser/SubtreeOperationsTest.php index 8375ac625f..f5929379fe 100644 --- a/tests/bundle/DependencyInjection/Configuration/Parser/SubtreeOperationsTest.php +++ b/tests/bundle/DependencyInjection/Configuration/Parser/SubtreeOperationsTest.php @@ -32,6 +32,16 @@ public function getExpectedCopySubtreeLimit(): iterable yield 'disabled = 0' => [0]; } + /** + * @return iterable + */ + public function getExpectedQuerySubtreeLimit(): iterable + { + yield 'no limit = -1' => [-1]; + yield 'custom limit = 1000' => [1000]; + yield 'disabled = 0' => [0]; + } + protected function setUp(): void { $this->parser = new SubtreeOperations(); @@ -77,4 +87,79 @@ public function testCopySubtreeLimitNotSet(): void $this->parser->mapConfig($scopeSettings, $currentScope, $this->contextualizer); } + + /** + * @dataProvider getExpectedQuerySubtreeLimit + */ + public function testQuerySubtreeLimit(int $expectedQuerySubtreeLimit): void + { + $scopeSettings = [ + 'subtree_operations' => [ + 'query_subtree' => [ + 'limit' => $expectedQuerySubtreeLimit, + ], + ], + ]; + $currentScope = 'admin_group'; + + $this->contextualizer + ->expects(self::once()) + ->method('setContextualParameter') + ->with( + 'subtree_operations.query_subtree.limit', + $currentScope, + $expectedQuerySubtreeLimit + ); + + $this->parser->mapConfig($scopeSettings, $currentScope, $this->contextualizer); + } + + public function testQuerySubtreeLimitNotSet(): void + { + $scopeSettings = [ + 'subtree_operations' => [ + 'query_subtree' => null, + ], + ]; + $currentScope = 'admin_group'; + + $this->contextualizer + ->expects(self::never()) + ->method('setContextualParameter'); + + $this->parser->mapConfig($scopeSettings, $currentScope, $this->contextualizer); + } + + public function testBothSubtreeOperationsSet(): void + { + $scopeSettings = [ + 'subtree_operations' => [ + 'copy_subtree' => [ + 'limit' => 200, + ], + 'query_subtree' => [ + 'limit' => 500, + ], + ], + ]; + $currentScope = 'admin_group'; + + $this->contextualizer + ->expects(self::exactly(2)) + ->method('setContextualParameter') + ->withConsecutive( + [ + 'subtree_operations.copy_subtree.limit', + $currentScope, + 200, + ], + [ + 'subtree_operations.query_subtree.limit', + $currentScope, + 500, + ] + ); + + $this->parser->mapConfig($scopeSettings, $currentScope, $this->contextualizer); + } }