diff --git a/bundles/AdminBundle/Controller/Admin/DataObject/DataObjectController.php b/bundles/AdminBundle/Controller/Admin/DataObject/DataObjectController.php index ee4d9a36194..6f083883be7 100644 --- a/bundles/AdminBundle/Controller/Admin/DataObject/DataObjectController.php +++ b/bundles/AdminBundle/Controller/Admin/DataObject/DataObjectController.php @@ -118,50 +118,32 @@ public function treeGetChildsByIdAction(Request $request, EventDispatcherInterfa $limit = 100; } - $childrenList = new DataObject\Listing(); - $childrenList->setCondition($this->buildChildrenCondition($object, $filter, $view)); - $childrenList->setLimit($limit); - $childrenList->setOffset($offset); - - if ($object->getChildrenSortBy() === 'index') { - $childrenList->setOrderKey('objects.o_index ASC', false); - } else { - $childrenList->setOrderKey( - sprintf( - 'CAST(objects.o_%s AS CHAR CHARACTER SET utf8) COLLATE utf8_general_ci %s', - $object->getChildrenSortBy(), $object->getChildrenSortOrder() - ), - false - ); - } - $childrenList->setObjectTypes($objectTypes); - - Element\Service::addTreeFilterJoins($cv, $childrenList); - - $beforeListLoadEvent = new GenericEvent($this, [ - 'list' => $childrenList, - 'context' => $allParams, - ]); - $eventDispatcher->dispatch($beforeListLoadEvent, AdminEvents::OBJECT_LIST_BEFORE_LIST_LOAD); - - /** @var DataObject\Listing $childrenList */ - $childrenList = $beforeListLoadEvent->getArgument('list'); - - $children = $childrenList->load(); - $filteredTotalCount = $childrenList->getTotalCount(); - - foreach ($children as $child) { - $objectTreeNode = $this->getTreeNodeConfig($child); - // this if is obsolete since as long as the change with #11714 about list on line 175-179 are working fine, we already filter the list=1 there - if ($objectTreeNode['permissions']['list'] == 1) { - $objects[] = $objectTreeNode; - } - } - - //pagination for custom view - $total = $cv - ? $filteredTotalCount - : $object->getChildAmount(null, $this->getAdminUser()); + // CANDO OPTIMIZATION START + // Because of the tree node locator is searching the tree with several pages per node with a binary search, + // which is triggered by the Admin UI JS. If there is a node with thousands of items, it makes a new request + // for every halving until the searched item was found between the offset and limit. + // To reduce the amount of requests this fix will get ID of the child node. If there are more items in the + // node as the limit it will recursive iterate through every page till the correct page is found. + // The original part of code is refactored into a separate function, which can be called recursively. + $childNodeId = $request->get('childNodeId', null); + $result = $this->getChildrenList( + $eventDispatcher, + $object, + $objectTypes, + $allParams, + $limit, + $offset, + $filter, + $view, + $cv, + $childNodeId + ); + // get the children list data and map them to the correct variables + $objects = $result['objects']; + $filteredTotalCount = $result['filteredTotalCount']; + $offset = $result['offset']; + $total = $result['total']; + // CANDO OPTIMIZATION END } //Hook for modifying return value - e.g. for changing permissions based on object data @@ -189,6 +171,107 @@ public function treeGetChildsByIdAction(Request $request, EventDispatcherInterfa return $this->adminJson($objects); } + // CANDO OPTIMIZATION START + private function getChildrenList( + EventDispatcherInterface $eventDispatcher, + DataObject $object, + array $objectTypes, + array $allParams, + int $limit, + int $offset, + ?string $filter, + ?string $view, + array|bool|null $cv, + ?string $childNodeId + ): array { + $objects = []; + $childrenList = new DataObject\Listing(); + $childrenList->setCondition($this->buildChildrenCondition($object, $filter, $view)); + $childrenList->setLimit($limit); + $childrenList->setOffset($offset); + + if ($object->getChildrenSortBy() === 'index') { + $childrenList->setOrderKey('objects.o_index ASC', false); + } else { + $childrenList->setOrderKey( + sprintf( + 'CAST(objects.o_%s AS CHAR CHARACTER SET utf8) COLLATE utf8_general_ci %s', + $object->getChildrenSortBy(), $object->getChildrenSortOrder() + ), + false + ); + } + $childrenList->setObjectTypes($objectTypes); + + Element\Service::addTreeFilterJoins($cv, $childrenList); + + $beforeListLoadEvent = new GenericEvent($this, [ + 'list' => $childrenList, + 'context' => $allParams, + ]); + $eventDispatcher->dispatch($beforeListLoadEvent, AdminEvents::OBJECT_LIST_BEFORE_LIST_LOAD); + + /** @var DataObject\Listing $childrenList */ + $childrenList = $beforeListLoadEvent->getArgument('list'); + + $children = $childrenList->load(); + $filteredTotalCount = $childrenList->getTotalCount(); + + // only execute the recursive call if the childNodeId is set + if (!empty($childNodeId)) { + // check if childNodeId is in current list, if not do it again by recalling this function + $found = false; + foreach ($children as $child) { + if ($child->getId() === (int) $childNodeId) { + $found = true; + break; + } + } + + // if the child node is not found, then call this function again + if (!$found) { + $nextOffset = $offset + $limit; + // if next offset is higher than the filtered total count, then the item was not found at all + if ($filteredTotalCount > $nextOffset) { + return $this->getChildrenList( + $eventDispatcher, + $object, + $objectTypes, + $allParams, + $limit, + $nextOffset, + $filter, + $view, + $cv, + $childNodeId + ); + } + } + } + + foreach ($children as $child) { + $objectTreeNode = $this->getTreeNodeConfig($child); + // this if is obsolete since as long as the change with #11714 about list on line 175-179 are working fine, we already filter the list=1 there + if ($objectTreeNode['permissions']['list'] == 1) { + $objects[] = $objectTreeNode; + } + } + + //pagination for custom view + $total = $cv + ? $filteredTotalCount + : $object->getChildAmount(null, $this->getAdminUser()); + + // return all calculated values to the calling function as an array + return [ + 'objects' => $objects, + 'filteredTotalCount' => $filteredTotalCount, + 'offset' => $offset, + 'total' => $total, + ]; + } + // CANDO OPTIMIZATION END + /** * @param DataObject\AbstractObject $object * @param string|null $filter @@ -1160,9 +1243,14 @@ private function executeUpdateAction(DataObject $object, mixed $values): array $object->save(); - if ($isIndexUpdate) { + // CANDO OPTIMIZATION START + // do update indexes only if parents childrenSortBy is set to index + // sort by index cannot be set on nodes with paged children anyway + // it doesn't make sense to sort by index on large amount of data in one node + if ($isIndexUpdate && $parent->getChildrenSortBy() === 'index') { $this->updateIndexesOfObjectSiblings($object, $indexUpdate); } + // CANDO OPTIMIZATION END $success = true; } catch (\Exception $e) { diff --git a/bundles/AdminBundle/Resources/public/js/pimcore/overrides.js b/bundles/AdminBundle/Resources/public/js/pimcore/overrides.js index f30ce1058d4..41691dd16b5 100644 --- a/bundles/AdminBundle/Resources/public/js/pimcore/overrides.js +++ b/bundles/AdminBundle/Resources/public/js/pimcore/overrides.js @@ -410,6 +410,14 @@ Ext.define('pimcore.data.PagingTreeStore', { me.superclass.onProxyLoad.call(this, operation); var proxy = this.getProxy(); proxy.setExtraParam("start", 0); + + // CANDO OPTIMIZATION START + // this line will reset the childNodeId to make sure it'll not interfere with other proxy requests + // childNodeId will be set in the treenodelocator.js and can be used within a custom listener + // to improve the performance on the client side + proxy.setExtraParam('childNodeId', null); + // CANDO OPTIMIZATION END + } catch (e) { console.log(e); } diff --git a/bundles/AdminBundle/Resources/public/js/pimcore/treenodelocator.js b/bundles/AdminBundle/Resources/public/js/pimcore/treenodelocator.js index e5ed82d3541..0fec1d85c54 100644 --- a/bundles/AdminBundle/Resources/public/js/pimcore/treenodelocator.js +++ b/bundles/AdminBundle/Resources/public/js/pimcore/treenodelocator.js @@ -421,6 +421,13 @@ pimcore.treenodelocator = function() proxy.setExtraParam("start", pagingState.offset); node.pagingData.offset = pagingState.offset; + // CANDO OPTIMIZATION START + // this line will set the childNodeId to add it to the backend request and can be used within + // a custom listener with the event pimcore.admin.object.list.beforeListLoad to improve the performance + // childNodeId will be reset in the treenodelocator.js + proxy.setExtraParam('childNodeId', pagingState.childNodeId); + // CANDO OPTIMIZATION END + store.load({ node: node, callback: self.processPaging