diff --git a/Block/Adminhtml/Category/AbstractCategory.php b/Block/Adminhtml/Category/AbstractCategory.php new file mode 100644 index 00000000..34312256 --- /dev/null +++ b/Block/Adminhtml/Category/AbstractCategory.php @@ -0,0 +1,293 @@ +_categoryTree = $categoryTree; + $this->_coreRegistry = $registry; + $this->_categoryFactory = $categoryFactory; + $this->collectionFactory = $collectionFactory; + $this->_withProductCount = true; + parent::__construct($context, $data); + } + + /** + * @return mixed|null + */ + public function getCategory() + { + if (null === $this->currentCategory) { + $categoryId = (int)$this->getRequest()->getParam('id'); + + if ($categoryId) { + $objectManager = \Magento\Framework\App\ObjectManager::getInstance(); + + $catRepo = $objectManager->create( \Magefan\Blog\Api\CategoryRepositoryInterface::class); + $this->currentCategory = $catRepo->getById($categoryId); + } + } + + return $this->currentCategory; + } + + /** + * Get category id + * + * @return int|string|null + */ + public function getCategoryId() + { + if ($this->getCategory()) { + return $this->getCategory()->getId(); + } + + return \Magefan\Blog\Model\Category::TREE_ROOT_ID; + } + + /** + * Get category name + * + * @return string + */ + public function getCategoryName() + { + return $this->getCategory()->getName(); + } + + /** + * Get category path + * + * @return mixed + */ + public function getCategoryPath() + { + if ($this->getCategory()) { + return $this->getCategory()->getPath(); + } + + return \Magefan\Blog\Model\Category::TREE_ROOT_ID; + } + + /** + * Check store root category + * + * @return bool + */ + public function hasStoreRootCategory() + { + $root = $this->getRoot(); + if ($root && $root->getId()) { + return true; + } + return false; + } + + /** + * Get store from request + * + * @return Store + */ + public function getStore() + { + $storeId = (int)$this->getRequest()->getParam('store'); + return $this->_storeManager->getStore($storeId); + } + + /** + * Get root category for tree + * + * @param mixed|null $parentNodeCategory + * @param int $recursionLevel + * @return Node|array|null + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + public function getRoot($parentNodeCategory = null, $recursionLevel = 3) + { + if ($parentNodeCategory !== null && $parentNodeCategory->getId()) { + return $this->getNode($parentNodeCategory, $recursionLevel); + } + + $root = $this->_coreRegistry->registry('root'); + if ($root === null) { + /*$storeId = (int)$this->getRequest()->getParam('store'); + + if ($storeId) { + $store = $this->_storeManager->getStore($storeId); + $rootId = $store->getRootCategoryId(); + } else { + $rootId = \Magefan\Blog\Model\Category::TREE_ROOT_ID; + }*/ + $rootId = \Magefan\Blog\Model\Category::TREE_ROOT_ID; + + $tree = $this->_categoryTree->load(null, $recursionLevel); + + if ($this->getCategory()) { + $tree->loadEnsuredNodes($this->getCategory(), $tree->getNodeById($rootId)); + } + + $tree->addCollectionData($this->getCategoryCollection()); + + $root = $tree->getNodeById($rootId); + + if ($root) { + $root->setIsVisible(true); + if ($root->getId() == \Magefan\Blog\Model\Category::TREE_ROOT_ID) { + $root->setName(__('Root')); + } + } + + $this->_coreRegistry->register('root', $root); + } + + return $root; + } + + /** + * Get Default Store Id + * + * @return int + */ + protected function _getDefaultStoreId() + { + return \Magento\Store\Model\Store::DEFAULT_STORE_ID; + } + + /** + * Get category collection + * + * @return \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection + */ + public function getCategoryCollection() + { + $storeId = $this->getRequest()->getParam('store', $this->_getDefaultStoreId()); + $collection = $this->getData('category_collection'); + if ($collection === null) { + $collection = $this->collectionFactory->create(); + + $collection + ->addFieldToSelect('category_id') + ->addFieldToSelect('title') + ->addFieldToSelect('is_active'); + + $this->setData('category_collection', $collection); + } + return $collection; + } + + /** + * Get category node for tree + * + * @param mixed $parentNodeCategory + * @param int $recursionLevel + * @return Node + */ + public function getNode($parentNodeCategory, $recursionLevel = 2) + { + $nodeId = $parentNodeCategory->getId(); + $node = $this->_categoryTree->loadNode($nodeId); + + $node->loadChildren($recursionLevel); + + if ($node && $nodeId != \Magefan\Blog\Model\Category::TREE_ROOT_ID) { + $node->setIsVisible(true); + } elseif ($node && $node->getId() == \Magefan\Blog\Model\Category::TREE_ROOT_ID) { + $node->setName(__('Root')); + } + + $this->_categoryTree->addCollectionData($this->getCategoryCollection()); + return $node; + } + + /** + * Get category save url + * + * @param array $args + * @return string + */ + public function getSaveUrl(array $args = []) + { + $params = ['_current' => false, '_query' => false, 'store' => $this->getStore()->getId()]; + $params = array_merge($params, $args); + return $this->getUrl('catalog/*/save', $params); + } + + /** + * Get category edit url + * + * @return string + */ + public function getEditUrl() + { + return $this->getUrl( + 'blog/category/edit', + ['store' => null, '_query' => false, 'id' => null, 'parent' => null] + ); + } + + /** + * Return ids of root categories as array + * + * @return array + */ + public function getRootIds() + { + return [\Magefan\Blog\Model\Category::TREE_ROOT_ID]; + } +} diff --git a/Block/Adminhtml/Category/Tree.php b/Block/Adminhtml/Category/Tree.php new file mode 100644 index 00000000..58318d4b --- /dev/null +++ b/Block/Adminhtml/Category/Tree.php @@ -0,0 +1,419 @@ +_jsonEncoder = $jsonEncoder; + $this->_resourceHelper = $resourceHelper; + $this->_backendSession = $backendSession; + parent::__construct($context, $categoryTree, $registry, $categoryFactory, $collectionFactory, $data); + $this->secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + } + + /** + * @inheritdoc + */ + protected function _construct() + { + parent::_construct(); + $this->setUseAjax(0); + } + + /** + * @inheritdoc + */ + protected function _prepareLayout() + { + $addUrl = $this->getUrl("*/*/new", ['_current' => false, 'id' => null, '_query' => false]); + if ($this->getStore()->getId() == Store::DEFAULT_STORE_ID) { + $this->addChild( + 'add_sub_button', + Button::class, + [ + 'label' => __('Add Subcategory'), + 'onclick' => "addNew('" . $addUrl . "', false)", + 'class' => 'add', + 'id' => 'add_subcategory_button', + 'style' => $this->canAddSubCategory() ? '' : 'display: none;' + ] + ); + + if ($this->canAddRootCategory()) { + $this->addChild( + 'add_root_button', + Button::class, + [ + 'label' => __('Add Root Category'), + 'onclick' => "addNew('" . $addUrl . "', true)", + 'class' => 'add', + 'id' => 'add_root_category_button' + ] + ); + } + } + + return parent::_prepareLayout(); + } + + + + /** + * Get add root button html + * + * @return string + */ + public function getAddRootButtonHtml() + { + return $this->getChildHtml('add_root_button'); + } + + /** + * Get add sub button html + * + * @return string + */ + public function getAddSubButtonHtml() + { + return $this->getChildHtml('add_sub_button'); + } + + /** + * Get expand button html + * + * @return string + */ + public function getExpandButtonHtml() + { + return $this->getChildHtml('expand_button'); + } + + /** + * Get collapse button html + * + * @return string + */ + public function getCollapseButtonHtml() + { + return $this->getChildHtml('collapse_button'); + } + + /** + * Get store switcher + * + * @return string + */ + public function getStoreSwitcherHtml() + { + return $this->getChildHtml('store_switcher'); + } + + /** + * Get loader tree url + * + * @param bool|null $expanded + * @return string + */ + public function getLoadTreeUrl($expanded = null) + { + $params = ['_current' => true, 'id' => null, 'store' => null]; + if ($expanded === null && $this->_backendSession->getIsTreeWasExpanded() || $expanded == true) { + $params['expand_all'] = true; + } + return $this->getUrl('*/*/categoriesJson', $params); + } + + /** + * Get nodes url + * + * @return string + */ + public function getNodesUrl() + { + return $this->getUrl('blog/category/tree'); + } + + /** + * Get switcher tree url + * + * @return string + */ + public function getSwitchTreeUrl() + { + return $this->getUrl( + 'blog/category/tree', + ['_current' => true, 'store' => null, '_query' => false, 'id' => null, 'parent' => null] + ); + } + + /** + * Get is was expanded + * + * @return bool + * @SuppressWarnings(PHPMD.BooleanGetMethodName) + */ + public function getIsWasExpanded() + { + return $this->_backendSession->getIsTreeWasExpanded(); + } + + /** + * Get move url + * + * @return string + */ + public function getMoveUrl() + { + return $this->getUrl('blog/category/move', ['store' => $this->getRequest()->getParam('store')]); + } + + /** + * Get tree + * + * @param mixed|null $parenNodeCategory + * @return array + */ + public function getTree($parenNodeCategory = null) + { + $rootArray = $this->_getNodeJson($this->getRoot($parenNodeCategory)); + $tree = $rootArray['children'] ?? []; + + return $tree; + } + + /** + * Get tree json + * + * @param mixed|null $parenNodeCategory + * @return string + */ + public function getTreeJson($parenNodeCategory = null) + { + $rootArray = $this->_getNodeJson($this->getRoot($parenNodeCategory)); + $json = $this->_jsonEncoder->encode($rootArray['children'] ?? []); + + return $json; + } + + /** + * Get JSON of a tree node or an associative array + * + * @param Node|array $node + * @param int $level + * @return array + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + protected function _getNodeJson($node, $level = 0) + { + // create a node from data array + if (is_array($node)) { + $node = new Node($node, 'entity_id', new \Magento\Framework\Data\Tree()); + } + + $item = []; + $item['text'] = $this->buildNodeName($node); + + $rootForStores = in_array($node->getEntityId(), $this->getRootIds()); + + $item['id'] = $node->getId(); + $item['store'] = (int)$this->getStore()->getId(); + $item['path'] = $node->getData('path'); + $item['a_attr'] = ['class' => $node->getIsActive() ? 'active-category' : 'not-active-category']; + $item['cls'] = 'folder ' . ($node->getIsActive() ? 'active-category' : 'no-active-category'); + + $allowMove = $this->_isCategoryMoveable($node); + $item['allowDrop'] = $allowMove; + // disallow drag if it's first level and category is root of a store + + $item['allowDrag'] = $allowMove && ($node->getLevel() == 1 && $rootForStores ? false : true); + + $isParent = $this->_isParentSelectedCategory($node); + + // used to identify if children may be loaded via ajax in jstree + // if you face some issue with child display - remove condition and just add $item['children'] = []; + if ((int)$node->getChildrenCount() > 0) { + $item['children'] = []; + } + + if ($node->hasChildren()) { + $item['children'] = []; + if (!($this->getUseAjax() && $node->getLevel() > 1 && !$isParent)) { + foreach ($node->getChildren() as $child) { + $item['children'][] = $this->_getNodeJson($child, $level + 1); + } + } + } + + if ($isParent || $node->getLevel() < 1) { + $item['expanded'] = true; + } + + return $item; + } + + /** + * Get category name + * + * @param DataObject $node + * @return string + */ + public function buildNodeName($node) + { + $result = $this->escapeHtml($node->getTitle()); + $result .= ' (ID: ' . $node->getId() . ')'; + + if ($this->_withProductCount) { + $result .= ' (' . $node->getPostCount() . ')'; + } + return $result; + } + + /** + * Is category movable + * + * @param Node|array $node + * @return bool + */ + protected function _isCategoryMoveable($node) + { + $options = new DataObject(['is_moveable' => true, 'category' => $node]); + + $this->_eventManager->dispatch('adminhtml_blog_category_tree_is_moveable', ['options' => $options]); + + return $options->getIsMoveable(); + } + + /** + * Is parent selected category + * + * @param Node|array $node + * @return bool + */ + protected function _isParentSelectedCategory($node) + { + if ($node && $this->getCategory()) { + $pathIds = $this->getCategory()->getPathIds(); + if (in_array($node->getId(), $pathIds)) { + return true; + } + } + + return false; + } + + /** + * Check if page loaded by outside link to category edit + * + * @return boolean + */ + public function isClearEdit() + { + return (bool)$this->getRequest()->getParam('clear'); + } + + /** + * Check availability of adding root category + * + * @return boolean + */ + public function canAddRootCategory() + { + $options = new DataObject(['is_allow' => true]); + $this->_eventManager->dispatch( + 'adminhtml_blog_category_tree_can_add_root_category', + ['category' => $this->getCategory(), 'options' => $options, 'store' => $this->getStore()->getId()] + ); + + return $options->getIsAllow(); + } + + /** + * Check availability of adding sub category + * + * @return boolean + */ + public function canAddSubCategory() + { + $options = new DataObject(['is_allow' => true]); + $this->_eventManager->dispatch( + 'adminhtml_blog_category_tree_can_add_sub_category', + ['category' => $this->getCategory(), 'options' => $options, 'store' => $this->getStore()->getId()] + ); + + return $options->getIsAllow(); + } +} diff --git a/Controller/Adminhtml/Category/CategoriesJson.php b/Controller/Adminhtml/Category/CategoriesJson.php new file mode 100644 index 00000000..ccd29972 --- /dev/null +++ b/Controller/Adminhtml/Category/CategoriesJson.php @@ -0,0 +1,90 @@ +resultJsonFactory = $resultJsonFactory; + $this->layoutFactory = $layoutFactory; + $this->authSession = $authSession ?: ObjectManager::getInstance() + ->get(\Magento\Backend\Model\Auth\Session::class); + } + + /** + * Get tree node (Ajax version) + * + * @return \Magento\Framework\Controller\ResultInterface + */ + public function execute() + { + if ($this->getRequest()->getParam('expand_all')) { + $this->authSession->setIsTreeWasExpanded(true); + } else { + $this->authSession->setIsTreeWasExpanded(false); + } + $categoryId = (int)$this->getRequest()->getPost('id'); + if ($categoryId) { + $this->getRequest()->setParam('id', $categoryId); + + $category = $this->_getModel(); + if (!$category) { + /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ + $resultRedirect = $this->resultRedirectFactory->create(); + return $resultRedirect->setPath('catalog/*/', ['_current' => true, 'id' => null]); + } + /** @var \Magento\Framework\Controller\Result\Json $resultJson */ + $resultJson = $this->resultJsonFactory->create(); + return $resultJson->setJsonData( + $this->layoutFactory->create()->createBlock(\Magefan\Blog\Block\Adminhtml\Category\Tree::class) + ->getTreeJson($category) + ); + } + } +} diff --git a/Controller/Adminhtml/Category/Move.php b/Controller/Adminhtml/Category/Move.php new file mode 100644 index 00000000..374fb91f --- /dev/null +++ b/Controller/Adminhtml/Category/Move.php @@ -0,0 +1,117 @@ +resultJsonFactory = $resultJsonFactory; + $this->layoutFactory = $layoutFactory; + $this->logger = $logger; + } + + /** + * Move category action + * + * @return \Magento\Framework\Controller\Result\Json + */ + public function execute() + { + /** + * New parent category identifier + */ + $parentNodeId = $this->getRequest()->getPost('pid', false); + /** + * Category id after which we have put our category + */ + $prevNodeId = $this->getRequest()->getPost('aid', false); + + /** @var $block \Magento\Framework\View\Element\Messages */ + $block = $this->layoutFactory->create()->getMessagesBlock(); + $error = false; + + try { + $categoryId = (int)$this->getRequest()->getPost('id'); + + if ($categoryId) { + $this->getRequest()->setParam('id', $categoryId); + + $category = $this->_getModel(); + + if ($category === false) { + throw new \Exception(__('Category is not available for requested store.')); + } + + $category->move($parentNodeId, $prevNodeId); + } + + } catch (\Magento\Framework\Exception\LocalizedException $e) { + $error = true; + $this->messageManager->addExceptionMessage($e); + } catch (\Exception $e) { + $error = true; + $this->messageManager->addErrorMessage(__('There was a category move error.')); + $this->logger->critical($e); + } + + if (!$error) { + $this->messageManager->addSuccessMessage(__('You moved the category.')); + } + + $block->setMessages($this->messageManager->getMessages(true)); + $resultJson = $this->resultJsonFactory->create(); + + return $resultJson->setData([ + 'messages' => $block->getGroupedHtml(), + 'error' => $error + ]); + } +} diff --git a/Controller/Adminhtml/Category/Save.php b/Controller/Adminhtml/Category/Save.php index 9caa17fb..37d497f6 100755 --- a/Controller/Adminhtml/Category/Save.php +++ b/Controller/Adminhtml/Category/Save.php @@ -36,6 +36,34 @@ protected function _afterSave($model, $request) protected function _beforeSave($model, $request) { + $categoryPostData = $request->getPostValue(); + + $isNewCategory = empty($categoryPostData['category_id']); + $parentId = $categoryPostData['parent'] ?? null; + + if ($parentId) { + $model->setParentId($parentId); + } + + if ($isNewCategory) { + $storeId = 0; + $parentCategory = $this->getParentCategory($parentId, $storeId); + + $path = $parentCategory->getPath(); + + if ($parentId) { + if ($path) { + $path .= '/' . $parentId; + } else { + $path = $parentId; + } + } + + $model->setPath($path); + $model->setParentId($parentCategory->getId()); + $model->setLevel(null); + } + /* Prepare images */ $this->prepareImagesBeforeSave($model, ['category_img']); } @@ -67,4 +95,29 @@ protected function filterParams($data) return $data; } + + + /** + * Get parent category + * + * @param int $parentId + * @param int $storeId + * + * @return \Magefan\Blog\Model\Category + */ + protected function getParentCategory($parentId, $storeId) + { + if (!$parentId) { + if ($storeId) { + //$parentId = $this->storeManager->getStore($storeId)->getRootCategoryId(); + } else { + $parentId = \Magento\Catalog\Model\Category::TREE_ROOT_ID; + return $this->_objectManager->create(\Magefan\Blog\Model\Category::class) + ->setId($parentId) + ->setPath(''); + } + } + + return $this->_objectManager->create(\Magefan\Blog\Model\Category::class)->load($parentId); + } } diff --git a/Model/Category.php b/Model/Category.php index 7e0a0001..b3617746 100755 --- a/Model/Category.php +++ b/Model/Category.php @@ -11,6 +11,7 @@ use Magefan\Blog\Model\Url; use Magento\Framework\DataObject\IdentityInterface; use Magefan\Blog\Api\ShortContentExtractorInterface; +use Magento\Framework\Exception\NoSuchEntityException; /** * Category model @@ -46,6 +47,9 @@ class Category extends \Magento\Framework\Model\AbstractModel implements Identit const STATUS_ENABLED = 1; const STATUS_DISABLED = 0; + + const TREE_ROOT_ID = 0; + /** * Prefix of model events names * @@ -341,6 +345,16 @@ public function getLevel() return count($this->getParentIds()); } + /** + * @return array + */ + public function getPathIds() + { + $pathIds = $this->getParentIds(); + $pathIds[] = $this->getId(); + return $pathIds; + } + /** * Retrieve catgegory url route path * @return string @@ -592,4 +606,58 @@ public function getCategoryImage() return $this->getData('category_image'); } + + /** + * Move category + * + * @param int $parentId new parent category id + * @param null|int $afterCategoryId category id after which we have put current category + * @return $this + * @throws \Magento\Framework\Exception\LocalizedException|\Exception + */ + public function move($parentId, $afterCategoryId) + { + try { + $parent = $this->loadFromRepository($parentId); + } catch (NoSuchEntityException $e) { + throw new \Magento\Framework\Exception\LocalizedException( + __('Sorry, but we can\'t find the new parent category you selected.'), $e + ); + } + + if (!$this->getId()) { + throw new \Magento\Framework\Exception\LocalizedException( + __('Sorry, but we can\'t find the new category you selected.') + ); + } elseif ($parent->getId() == $this->getId()) { + throw new \Magento\Framework\Exception\LocalizedException( + __('We can\'t move the category because the parent category name matches the child category name.') + ); + } + + $eventParams = [ + $this->_eventObject => $this, + 'parent' => $parent, + 'category_id' => $this->getId(), + 'prev_parent_id' => $this->getParentId(), + 'parent_id' => $parentId + ]; + + $this->_getResource()->beginTransaction(); + try { + $this->_eventManager->dispatch($this->_eventPrefix . '_move_before', $eventParams); + $this->getResource()->changeParent($this, $parent, $afterCategoryId); + $this->_eventManager->dispatch($this->_eventPrefix . '_move_after', $eventParams); + $this->_getResource()->commit(); + + } catch (\Exception $e) { + $this->_getResource()->rollBack(); + throw $e; + } + $this->_eventManager->dispatch('blog_category_move', $eventParams); + $this->_eventManager->dispatch('clean_cache_by_tags', ['object' => $this]); + $this->_cacheManager->clean([self::CACHE_TAG]); + + return $this; + } } diff --git a/Model/Import/AbstractImport.php b/Model/Import/AbstractImport.php index 1af6e1c9..b581af4d 100644 --- a/Model/Import/AbstractImport.php +++ b/Model/Import/AbstractImport.php @@ -275,7 +275,7 @@ protected function getDbAdapter() } catch (\Exception $e) { throw new \Exception("Failed connect to the database."); } - + } return $this->dbAdapter; } @@ -302,7 +302,7 @@ protected function getFeaturedImgBySrc($src) if (!$hasFormat) { $imageName .= '.jpg'; } - + $imagePath = $mediaPath . '/' . $imageName; $imageSource = false; if (!$this->file->fileExists($imagePath)) { diff --git a/Model/ResourceModel/Category.php b/Model/ResourceModel/Category.php index 68f184fc..27bd9cad 100755 --- a/Model/ResourceModel/Category.php +++ b/Model/ResourceModel/Category.php @@ -33,7 +33,7 @@ class Category extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb public function __construct( \Magento\Framework\Model\ResourceModel\Db\Context $context, \Magento\Framework\Stdlib\DateTime $dateTime, - $resourcePrefix = null + $resourcePrefix = null ) { parent::__construct($context, $resourcePrefix); $this->dateTime = $dateTime; @@ -81,7 +81,7 @@ protected function _beforeSave(\Magento\Framework\Model\AbstractModel $object) } $identifierGenerator = \Magento\Framework\App\ObjectManager::getInstance() - ->create(\Magefan\Blog\Model\ResourceModel\PageIdentifierGenerator::class); + ->create(\Magefan\Blog\Model\ResourceModel\PageIdentifierGenerator::class); $identifierGenerator->generate($object); if (!$this->isValidPageIdentifier($object)) { @@ -103,6 +103,30 @@ protected function _beforeSave(\Magento\Framework\Model\AbstractModel $object) ); } + if ($object->isObjectNew()) { + if ($object->getPosition() === null) { + $object->setPosition($this->_getMaxPosition($object->getPath()) + 1); + } + + $path = explode('/', (string)$object->getPath()); + $level = $object->getPath() ? count($path) + 1 : 1; + + $toUpdateChild = array_diff($path, [$object->getId()]); + + if (!$object->hasPosition()) { + $object->setPosition($this->_getMaxPosition(implode('/', $toUpdateChild)) + 1); + } + if (!$object->hasLevel()) { + $object->setLevel($level); + } +/* + var_dump($object->getData('level')); + var_dump($object->getData('position')); + var_dump($object->getData('path')); + exit();*/ + } + + return parent::_beforeSave($object); } @@ -292,4 +316,140 @@ public function getEntityType() { return 'category'; } + + /** + * Get maximum position of child categories by specific tree path + * + * @param string $path + * @return int + */ + protected function _getMaxPosition($path) + { + $connection = $this->getConnection(); + $positionField = $connection->quoteIdentifier('position'); + $level = count(explode('/', (string)$path)); + $bind = ['c_level' => $level, 'c_path' => $path . '/%']; + $select = $connection->select()->from( + $this->getTable($this->getMainTable()), + 'MAX(' . $positionField . ')' + )->where( + $connection->quoteIdentifier('path') . ' LIKE :c_path' + )->where( + $connection->quoteIdentifier('level') . ' = :c_level' + ); + + $position = $connection->fetchOne($select, $bind); + if (!$position) { + $position = 0; + } + return $position; + } + + /** + * Move category to another parent node + * + * @param \Magefan\Blog\Model\Category $category + * @param \Magefan\Blog\Model\Category $newParent + * @param null|int $afterCategoryId + * @return $this + */ + public function changeParent( + \Magefan\Blog\Model\Category $category, + \Magefan\Blog\Model\Category $newParent, + $afterCategoryId = null + ) { + $table = $this->getMainTable(); + $connection = $this->getConnection(); + $levelField = $connection->quoteIdentifier('level'); + $pathField = $connection->quoteIdentifier('path'); + + $position = $this->_processPositions($category, $newParent, $afterCategoryId); + + $newPath = sprintf('%s/%s', $newParent->getPath(), $newParent->getId()); + + $newLevel = $newParent->getLevel() + 2; + $levelDisposition = $newLevel - $category->getLevel(); + + /** + * Update children nodes path + */ + $connection->update( + $table, + [ + 'path' => new \Zend_Db_Expr( + 'REPLACE(' . $pathField . ',' . $connection->quote( + $category->getPath() . '/' + ) . ', ' . $connection->quote( + $newPath . '/' + ) . ')' + ), + 'level' => new \Zend_Db_Expr($levelField . ' + ' . $levelDisposition) + ], + [$pathField . ' LIKE ?' => $category->getPath() . '/%'] + ); + /** + * Update moved category data + */ + $data = [ + 'path' => $newPath, + 'level' => $newLevel, + 'position' => $position, + // 'parent_id' => $newParent->getId(), + ]; + $connection->update($table, $data, ['category_id = ?' => $category->getId()]); + + // Update category object to new data + $category->addData($data); + $category->unsetData('path_ids'); + + return $this; + } + + /** + * Process positions of old parent category children and new parent category children. + * + * Get position for moved category + * + * @param \Magefan\Blog\Model\Category $category + * @param \Magefan\Blog\Model\Category $newParent + * @param null|int $afterCategoryId + * @return int + */ + protected function _processPositions($category, $newParent, $afterCategoryId) + { + $table = $this->getMainTable(); + $connection = $this->getConnection(); + $positionField = $connection->quoteIdentifier('position'); + + $bind = ['position' => new \Zend_Db_Expr($positionField . ' - 1')]; + $where = [ + 'path LIKE ?' => "%\\{$category->getParentId()}", + $positionField . ' > ?' => $category->getPosition(), + ]; + $connection->update($table, $bind, $where); + + /** + * Prepare position value + */ + if ($afterCategoryId) { + $select = $connection->select()->from($table, 'position')->where('category_id = :category_id'); + $position = $connection->fetchOne($select, ['category_id' => $afterCategoryId]); + $position = (int)$position + 1; + } else { + $position = 1; + } + + $bind = ['position' => new \Zend_Db_Expr($positionField . ' + 1')]; + $where = [ + // 'parent_id = ?' => $newParent->getId(), + 'path LIKE ?' => "%\\{$newParent->getId()}", + $positionField . ' >= ?' => $position + ]; + $connection->update($table, $bind, $where); + + return $position; + } + + + } diff --git a/Model/ResourceModel/Category/Tree.php b/Model/ResourceModel/Category/Tree.php new file mode 100644 index 00000000..03512446 --- /dev/null +++ b/Model/ResourceModel/Category/Tree.php @@ -0,0 +1,448 @@ +_blogCategory = $blogCategory; + $this->_cache = $cache; + $this->_storeManager = $storeManager; + $this->_coreResource = $resource; + parent::__construct( + $resource->getConnection(), + $resource->getTableName('magefan_blog_category'), + [ + Dbp::ID_FIELD => 'category_id', + Dbp::PATH_FIELD => 'path', + Dbp::ORDER_FIELD => 'position', + Dbp::LEVEL_FIELD => 'level' + ] + ); + $this->_eventManager = $eventManager; + $this->_collectionFactory = $collectionFactory; + } + + /** + * Load tree + * + * @param int|Node|string $parentNode + * @param int $recursionLevel + * @return $this + */ + public function load($parentNode = null, $recursionLevel = 0) + { + if (!$this->_loaded) { + $startLevel = 1; + $parentPath = ''; + + if ($parentNode instanceof Node) { + $parentPath = $parentNode->getData($this->_pathField); + $startLevel = $parentNode->getData($this->_levelField); + } elseif (is_numeric($parentNode)) { + $select = $this->_conn->select() + ->from($this->_table, [$this->_pathField, $this->_levelField]) + ->where("{$this->_idField} = ?", $parentNode); + $parent = $this->_conn->fetchRow($select); + + $startLevel = $parent[$this->_levelField]; + $parentPath = $parent[$this->_pathField]; + $parentNode = null; + } elseif (is_string($parentNode)) { + $parentPath = $parentNode; + $startLevel = count(explode(',', $parentPath)) - 1; + $parentNode = null; + } + + $select = clone $this->_select; + + $select->order($this->_table . '.' . $this->_orderField . ' ASC'); + if ($parentPath) { + $pathField = $this->_conn->quoteIdentifier([$this->_table, $this->_pathField]); + + $like = explode('/', $parentPath); + array_pop($like); + $like = implode('/', $like); + + $select->where("{$pathField} LIKE ?", "{$like}/%"); + } + if ($recursionLevel != 0) { + $levelField = $this->_conn->quoteIdentifier([$this->_table, $this->_levelField]); + $select->where("{$levelField} <= ?", $startLevel + $recursionLevel); + } + + $arrNodes = $this->_conn->fetchAll($select); + + $childrenItems = []; + + $dataRoot = [ + 'category_id' => 0 + ]; + + array_unshift($arrNodes, $dataRoot); + + foreach ($arrNodes as $nodeInfo) { + if (!empty($nodeInfo['category_id'])) { + if (empty($nodeInfo['path'])) { + $nodeInfo['path'] = $nodeInfo['category_id']; + } else { + $nodeInfo['path'] .= '/'. $nodeInfo['category_id']; + } + } + + $nodeInfo['post_count'] = $this->getCategoryPostsCount((int)$nodeInfo['category_id']); + $nodeInfo['children_count'] = $this->getCategoryChildrenCount((int)$nodeInfo['category_id']); + + $pathToParent = explode('/', $nodeInfo[$this->_pathField] ?? ''); + array_pop($pathToParent); + $pathToParent = implode('/', $pathToParent); + + if (isset($nodeInfo['level']) && $pathToParent == '') { + $pathToParent = '0'; + } + + $childrenItems[$pathToParent][] = $nodeInfo; + } + + $this->addChildNodes($childrenItems, $parentPath, $parentNode); + + $this->_loaded = true; + } + + return $this; + } + + /** + * Load ensured nodes + * + * @param object $category + * @param Node $rootNode + * @return void + */ + public function loadEnsuredNodes($category, $rootNode) + { + $pathIds = $category->getPathIds(); + $rootNodeId = $rootNode->getId(); + $rootNodePath = $rootNode->getData($this->_pathField); + + $select = clone $this->_select; + $select->order($this->_table . '.' . $this->_orderField . ' ASC'); + + if ($pathIds) { + $condition = $this->_conn->quoteInto("{$this->_table}.{$this->_idField} in (?)", $pathIds); + $select->where($condition); + } + + $arrNodes = $this->_conn->fetchAll($select); + + if ($arrNodes) { + $childrenItems = []; + foreach ($arrNodes as $nodeInfo) { + + if (!empty($nodeInfo['category_id'])) { + if (empty($nodeInfo['path'])) { + $nodeInfo['path'] = $nodeInfo['category_id']; + } else { + $nodeInfo['path'] .= '/'. $nodeInfo['category_id']; + } + } + + $nodeId = $nodeInfo[$this->_idField]; + if ($nodeId <= $rootNodeId) { + continue; + } + + $pathToParent = explode('/', $nodeInfo[$this->_pathField] ?? ''); + array_pop($pathToParent); + $pathToParent = implode('/', $pathToParent); + + $childrenItems[$pathToParent][] = $nodeInfo; + } + + $this->_addChildNodes($childrenItems, $rootNodePath, $rootNode, true); + } + } + + /** + * @param int $categoryId + * @return int + */ + private function getCategoryPostsCount(int $categoryId): int + { + if (null === $this->categoryPostsCount) { + $select = $this->_conn->select() + ->from( + $this->_coreResource->getTableName('magefan_blog_post_category'), + ['category_id', 'post_count' => new \Zend_Db_Expr('COUNT(post_id)')] + ) + ->group('category_id'); + + $this->categoryPostsCount = $this->_conn->fetchPairs($select); + } + + return $this->categoryPostsCount[$categoryId] ?? 0; + } + + /** + * @param int $categoryId + * @return int + */ + private function getCategoryChildrenCount(int $categoryId): int + { + if (null === $this->categoryChildrenCount) { + $tableName = $this->_coreResource->getTableName('magefan_blog_category'); + + // Fetch all categories with their path + $select = $this->_conn->select() + ->from($tableName, ['category_id', 'path']); + + $rows = $this->_conn->fetchAll($select); + + $collectData = []; + + foreach ($rows as $row) { + $path = (string)$row['path']; + $parts = explode('/', $path); + + foreach ($parts as $level => $catId) { + if (isset($collectData[$level][$catId])) { + $collectData[$level][$catId]++; + } else { + $collectData[$level][$catId] = 0; + } + } + } + + foreach ($collectData as $level => $categories) { + foreach ($categories as $catId => $count) { + if (isset($this->categoryChildrenCount[$catId])) { + $this->categoryChildrenCount[$catId] += $count; + } else { + $this->categoryChildrenCount[$catId] = $count; + } + } + } + } + + return $this->categoryChildrenCount[$categoryId] ?? 0; + } + + /** + * Set store id + * + * @param integer $storeId + * @return \Magefan\Blog\Model\ResourceModel\Category\Tree + */ + public function setStoreId($storeId) + { + $this->_storeId = (int)$storeId; + return $this; + } + + /** + * Return store id + * + * @return integer + */ + public function getStoreId() + { + if ($this->_storeId === null) { + $this->_storeId = $this->_storeManager->getStore()->getId(); + } + return $this->_storeId; + } + + /** + * @param $collection + * @return $this + */ + public function addCollectionData($collection = null) + { + if ($collection === null) { + $collection = $this->getCollection(); + } else { + $this->setCollection($collection); + } + + $nodeIds = []; + + foreach ($this->getNodes() as $node) { + $nodeIds[] = $node->getId(); + } + + $collection->addFieldToFilter('category_id', ['in' => $nodeIds]); + + return $this; + } + + + /** + * Get categories collection + * + * @param boolean $sorted + * @return Collection + */ + public function getCollection($sorted = false) + { + if ($this->_collection === null) { + $this->_collection = $this->_getDefaultCollection($sorted); + } + return $this->_collection; + } + + /** + * Clean unneeded collection + * + * @param Collection|array $object + * @return void + */ + protected function _clean($object) + { + if (is_array($object)) { + foreach ($object as $obj) { + $this->_clean($obj); + } + } + unset($object); + } + + /** + * Enter description here... + * + * @param Collection $collection + * @return $this + */ + public function setCollection($collection) + { + if ($this->_collection !== null) { + $this->_clean($this->_collection); + } + $this->_collection = $collection; + return $this; + } + + /** + * Enter description here... + * + * @param boolean $sorted + * @return Collection + */ + protected function _getDefaultCollection($sorted = false) + { + $this->_joinUrlRewriteIntoCollection = true; + $collection = $this->_collectionFactory->create(); + $collection->addAttributeToSelect('title'); + + if ($sorted) { + if (is_string($sorted)) { + // $sorted is supposed to be attribute name + $collection->addAttributeToSort($sorted); + } else { + $collection->addAttributeToSort('name'); + } + } + + return $collection; + } + + /** + * Executing parents move method and cleaning cache after it + * + * @param mixed $category + * @param mixed $newParent + * @param mixed $prevNode + * @return void + */ + public function move($category, $newParent, $prevNode = null) + { + $this->_blogCategory->move($category->getId(), $newParent->getId()); + parent::move($category, $newParent, $prevNode); + + $this->_afterMove(); + } + + /** + * Move tree after + * + * @return $this + */ + protected function _afterMove() + { + $this->_cache->clean([\Magefan\Blog\Model\Category::CACHE_TAG]); + return $this; + } +} diff --git a/Model/ResourceModel/Comment/Collection/Grid.php b/Model/ResourceModel/Comment/Collection/Grid.php index bd65f6c1..e17c1181 100755 --- a/Model/ResourceModel/Comment/Collection/Grid.php +++ b/Model/ResourceModel/Comment/Collection/Grid.php @@ -21,7 +21,7 @@ class Grid extends Collection * @var int */ protected $_storeId; - + /** * @var bool */ diff --git a/Ui/DataProvider/Category/Form/CategoryDataProvider.php b/Ui/DataProvider/Category/Form/CategoryDataProvider.php index 276b5978..783cc6f0 100644 --- a/Ui/DataProvider/Category/Form/CategoryDataProvider.php +++ b/Ui/DataProvider/Category/Form/CategoryDataProvider.php @@ -9,6 +9,7 @@ use Magefan\Blog\Model\ResourceModel\Category\CollectionFactory; use Magento\Framework\App\Request\DataPersistorInterface; +use Magento\Framework\App\RequestInterface; /** * Class DataProvider @@ -31,25 +32,33 @@ class CategoryDataProvider extends \Magento\Ui\DataProvider\AbstractDataProvider protected $loadedData; /** - * @param string $name - * @param string $primaryFieldName - * @param string $requestFieldName + * @var RequestInterface + */ + protected $request; + + /** + * @param $name + * @param $primaryFieldName + * @param $requestFieldName * @param CollectionFactory $categoryCollectionFactory * @param DataPersistorInterface $dataPersistor + * @param RequestInterface $request * @param array $meta * @param array $data */ public function __construct( - $name, - $primaryFieldName, - $requestFieldName, + string $name, + string $primaryFieldName, + string $requestFieldName, CollectionFactory $categoryCollectionFactory, DataPersistorInterface $dataPersistor, + RequestInterface $request, array $meta = [], array $data = [] ) { $this->collection = $categoryCollectionFactory->create(); $this->dataPersistor = $dataPersistor; + $this->request = $request; parent::__construct($name, $primaryFieldName, $requestFieldName, $meta, $data); $this->meta = $this->prepareMeta($this->meta); } @@ -62,7 +71,20 @@ public function __construct( */ public function prepareMeta(array $meta) { - return $meta; + $parent =(int)$this->request->getParam('parent'); + + $meta['general']['children']['parent'] = [ + 'arguments' => [ + 'data' => [ + 'config' => [ + 'default' => $parent + ], + ], + ] + ]; + + + return $meta; } /** diff --git a/etc/db_schema.xml b/etc/db_schema.xml index 28bb63c9..035b4ebb 100644 --- a/etc/db_schema.xml +++ b/etc/db_schema.xml @@ -102,6 +102,7 @@