diff --git a/CHANGELOG.md b/CHANGELOG.md index 16c6f018..709c0c58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +## [2.22.0] - 2023-05-08 +### Fixed +- Get product title method ([#270](https://github.com/SnowdogApps/magento2-menu/pull/270)) +- Category Child node without parent category chosen makes menu disappear (DEV-99318) + +### Changed +- Change Custom URL without URL content to not clickable (DEV-99301) + +### Added +- Error skipping mechanism for some exceptions ([#271](https://github.com/SnowdogApps/magento2-menu/pull/271)) +- Required prop for category child field (DEV-99318) +- Display a message indicating success or error after saving the menu. #274 + ## [2.21.0] - 2023-04-03 ### Fixed - Reset button doesn't reset menu node structure ([#256](https://github.com/SnowdogApps/magento2-menu/pull/256)) diff --git a/Controller/Adminhtml/Menu/ImportPost.php b/Controller/Adminhtml/Menu/ImportPost.php index b79a42ba..8a724348 100755 --- a/Controller/Adminhtml/Menu/ImportPost.php +++ b/Controller/Adminhtml/Menu/ImportPost.php @@ -13,6 +13,7 @@ use Snowdog\Menu\Model\ImportExport\File\Upload as FileUpload; use Snowdog\Menu\Model\ImportExport\Processor\Import as ImportProcessor; use Snowdog\Menu\Model\ImportExport\Processor\Import\Validator\ValidationAggregateError; +use Throwable; class ImportPost extends Action implements HttpPostActionInterface { @@ -67,7 +68,7 @@ public function execute() $exception->flush(); } catch (ValidatorException $exception) { $this->messageManager->addErrorMessage($exception->getMessage()); - } catch (Exception $exception) { + } catch (Exception|Throwable $exception) { $this->logger->critical($exception); $this->messageManager->addErrorMessage(__('An error occurred while importing menu.')); } diff --git a/Controller/Adminhtml/Menu/Save.php b/Controller/Adminhtml/Menu/Save.php index b24a2ad0..98520455 100644 --- a/Controller/Adminhtml/Menu/Save.php +++ b/Controller/Adminhtml/Menu/Save.php @@ -4,10 +4,15 @@ namespace Snowdog\Menu\Controller\Adminhtml\Menu; +use Exception; use Magento\Backend\App\Action\Context; use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\Controller\ResultInterface; +use Magento\Framework\Exception\AlreadyExistsException; +use Magento\Framework\Exception\CouldNotSaveException; +use Magento\Framework\Phrase; use Magento\Store\Model\StoreManagerInterface; +use Psr\Log\LoggerInterface; use Snowdog\Menu\Api\MenuRepositoryInterface; use Snowdog\Menu\Api\Data\MenuInterface; use Snowdog\Menu\Controller\Adminhtml\MenuAction; @@ -40,6 +45,11 @@ class Save extends MenuAction implements HttpPostActionInterface */ private $storeManager; + /** + * @var LoggerInterface + */ + private $logger; + public function __construct( Context $context, MenuRepositoryInterface $menuRepository, @@ -47,12 +57,14 @@ public function __construct( CloneRequestProcessor $cloneRequestProcessor, MenuHydrator $hydrator, MenuSaveRequestProcessor $menuSaveRequestProcessor, - StoreManagerInterface $storeManager + StoreManagerInterface $storeManager, + LoggerInterface $logger ) { $this->cloneRequestProcessor = $cloneRequestProcessor; $this->hydrator = $hydrator; $this->menuSaveRequestProcessor = $menuSaveRequestProcessor; $this->storeManager = $storeManager; + $this->logger = $logger; parent::__construct($context, $menuRepository, $menuFactory); } @@ -68,10 +80,8 @@ public function execute() $nodes = $nodes ? json_decode($nodes, true) : []; $this->hydrator->mapRequest($menu, $request); - $this->menuRepository->save($menu); - $menu->saveStores($this->getStores()); - $this->menuSaveRequestProcessor->saveData($menu, $nodes); + $this->processSave($menu, $nodes); return $this->processRedirect($menu); } @@ -106,4 +116,23 @@ private function getStores(): array return $stores; } + + private function processSave(MenuInterface $menu, $nodes): void + { + try { + $this->menuRepository->save($menu); + $menu->saveStores($this->getStores()); + + $this->menuSaveRequestProcessor->saveData($menu, $nodes); + + if (empty($this->messageManager->getMessages()->getErrors())) { + $this->messageManager->addSuccessMessage(new Phrase('Menu has been saved successfully.')); + } + } catch (AlreadyExistsException|CouldNotSaveException $e) { + $this->messageManager->addErrorMessage(new Phrase('Could not save Menu: %1', [$e->getMessage()])); + } catch (Exception $e) { + $this->logger->critical($e); + $this->messageManager->addErrorMessage(new Phrase('An error occurred while saving menu.')); + } + } } diff --git a/Model/ImportExport/Processor/Import.php b/Model/ImportExport/Processor/Import.php index a2d7441f..1d2c021f 100644 --- a/Model/ImportExport/Processor/Import.php +++ b/Model/ImportExport/Processor/Import.php @@ -4,11 +4,13 @@ namespace Snowdog\Menu\Model\ImportExport\Processor; +use Magento\Framework\App\Config\ScopeConfigInterface; use Snowdog\Menu\Api\Data\MenuInterface; -use Snowdog\Menu\Model\ImportExport\Processor\ExtendedFields; +use Snowdog\Menu\Model\ImportExport\Processor\Import\InvalidNodes as InvalidNodesProcessor; use Snowdog\Menu\Model\ImportExport\Processor\Import\Menu as MenuProcessor; use Snowdog\Menu\Model\ImportExport\Processor\Import\Node as NodeProcessor; use Snowdog\Menu\Model\ImportExport\Processor\Import\Validator\ValidationAggregateError; +use Snowdog\Menu\Model\ImportExport\Processor\Import\Validator\ValidationException; class Import { @@ -27,19 +29,36 @@ class Import */ private $validationAggregateError; + /** + * @var InvalidNodesProcessor + */ + private $invalidNodesProcessor; + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + public function __construct( MenuProcessor $menuProcessor, NodeProcessor $nodeProcessor, - ValidationAggregateError $validationAggregateError + ValidationAggregateError $validationAggregateError, + InvalidNodesProcessor $invalidNodesProcessor, + ScopeConfigInterface $scopeConfig ) { $this->menuProcessor = $menuProcessor; $this->nodeProcessor = $nodeProcessor; $this->validationAggregateError = $validationAggregateError; + $this->invalidNodesProcessor = $invalidNodesProcessor; + $this->scopeConfig = $scopeConfig; } + /** + * @throws ValidationAggregateError + */ public function importData(array $data): string { $this->validateData($data); + $menu = $this->createMenu($data); if (isset($data[ExtendedFields::NODES])) { @@ -63,7 +82,7 @@ private function createMenu(array $data): MenuInterface /** * @throws ValidationAggregateError */ - private function validateData(array $data): void + private function validateData(array &$data): void { $this->menuProcessor->validateImportData($data); @@ -71,8 +90,26 @@ private function validateData(array $data): void $this->nodeProcessor->validateImportData($data[ExtendedFields::NODES]); } - if ($this->validationAggregateError->getErrors()) { + if (empty($this->scopeConfig->getValue('snowmenu/import/strip_invalid_nodes')) + && $this->validationAggregateError->getErrors() + ) { throw $this->validationAggregateError; } + + $this->checkExceptionTypes($this->validationAggregateError); + $this->invalidNodesProcessor->process($data, $this->validationAggregateError); + } + + /** + * Rethrows $e if there's at least one error not matching ValidationException + * @throws ValidationAggregateError + */ + private function checkExceptionTypes(ValidationAggregateError $e) + { + foreach ($e->getErrors() as $error) { + if (!($error instanceof ValidationException)) { + throw $e; + } + } } } diff --git a/Model/ImportExport/Processor/Import/InvalidNodes.php b/Model/ImportExport/Processor/Import/InvalidNodes.php new file mode 100644 index 00000000..0442f246 --- /dev/null +++ b/Model/ImportExport/Processor/Import/InvalidNodes.php @@ -0,0 +1,84 @@ +treeTrace = $treeTrace; + $this->manager = $manager; + } + + public function process(array &$data, ValidationAggregateError $error): array + { + return $this->stripInvalidNodes($data, $error); + } + + /** + * @param array $data + * @param ValidationAggregateError $validationAggregateError + * @return array + */ + private function stripInvalidNodes(array &$data, ValidationAggregateError $validationAggregateError): array + { + foreach ($validationAggregateError->getErrors() as $error) { + if ($error instanceof ValidationException) { + if (empty($error->getInvalidNodePath())) { + continue; + } + + $this->unsetItemByPath($error->getInvalidNodePath(), $data); + $this->manager->addNoticeMessage(__( + "Invalid node %1 not imported. %2", + implode(' > ', $error->getInvalidNodePath()), + $error->getMessage() + )); + } + } + return $data; + } + + /** + * Unsets specific $data element by $path + */ + private function unsetItemByPath(array $path, array &$data): void + { + $path = $this->updatePathValues($path); + + $dataElement =& $data; + $lastItemKey = array_pop($path); + + foreach ($path as $key) { + $dataElement =& $dataElement["nodes"][$key]; + } + + $dataElement["nodes"][$lastItemKey] = null; + } + + private function updatePathValues(array $path): array + { + if ($this->treeTrace->isEnabledNodeIdAddend()) { + foreach ($path as &$idx) { + $idx--; + } + } + + return $path; + } +} diff --git a/Model/ImportExport/Processor/Import/Node.php b/Model/ImportExport/Processor/Import/Node.php index 92004797..286f4e37 100644 --- a/Model/ImportExport/Processor/Import/Node.php +++ b/Model/ImportExport/Processor/Import/Node.php @@ -68,6 +68,10 @@ public function createNodes( ?int $parentId = null ): void { foreach ($nodes as $nodeData) { + if ($nodeData === null) { + continue; + } + $node = $this->nodeFactory->create(); $data = $this->dataProcessor->getData($nodeData, $menuId, $level, $position++, $parentId); diff --git a/Model/ImportExport/Processor/Import/Node/Validator/NodeType.php b/Model/ImportExport/Processor/Import/Node/Validator/NodeType.php index 1f9148ca..c3ed3d9b 100644 --- a/Model/ImportExport/Processor/Import/Node/Validator/NodeType.php +++ b/Model/ImportExport/Processor/Import/Node/Validator/NodeType.php @@ -9,6 +9,7 @@ use Snowdog\Menu\Model\ImportExport\Processor\Import\Node\Type\Cms; use Snowdog\Menu\Model\ImportExport\Processor\Import\Node\Validator\TreeTrace; use Snowdog\Menu\Model\ImportExport\Processor\Import\Validator\ValidationAggregateError; +use Snowdog\Menu\Model\ImportExport\Processor\Import\Validator\ValidationException; use Snowdog\Menu\Model\ImportExport\Processor\NodeTypes; use Snowdog\Menu\Model\NodeTypeProvider; @@ -122,12 +123,18 @@ private function validateNodeTypeContent(array $node, $nodeId, array $treeTrace) } if (!$isValid) { + $treeTraceBreadcrumbs = $this->treeTrace->getBreadcrumbs($treeTrace, $nodeId); $this->validationAggregateError->addError( - __( - 'Node "%1" %2 identifier "%3" is invalid.', - $this->treeTrace->getBreadcrumbs($treeTrace, $nodeId), - $node[NodeInterface::TYPE], - $node[NodeInterface::CONTENT] + new ValidationException( + __( + 'Node "%1" %2 identifier "%3" is invalid.', + $treeTraceBreadcrumbs, + $node[NodeInterface::TYPE], + $node[NodeInterface::CONTENT] + ), + null, + 0, + explode(' > ', $treeTraceBreadcrumbs) ) ); } diff --git a/Model/ImportExport/Processor/Import/Node/Validator/TreeTrace.php b/Model/ImportExport/Processor/Import/Node/Validator/TreeTrace.php index d3b2ddf0..25316d65 100644 --- a/Model/ImportExport/Processor/Import/Node/Validator/TreeTrace.php +++ b/Model/ImportExport/Processor/Import/Node/Validator/TreeTrace.php @@ -46,4 +46,9 @@ public function disableNodeIdAddend(): void { $this->nodeIdAddend = 0; } + + public function isEnabledNodeIdAddend(): bool + { + return (bool) $this->nodeIdAddend; + } } diff --git a/Model/ImportExport/Processor/Import/Validator/ValidationAggregateError.php b/Model/ImportExport/Processor/Import/Validator/ValidationAggregateError.php index 454b9a44..8beb5dd3 100644 --- a/Model/ImportExport/Processor/Import/Validator/ValidationAggregateError.php +++ b/Model/ImportExport/Processor/Import/Validator/ValidationAggregateError.php @@ -6,6 +6,7 @@ use Exception; use Magento\Framework\Message\ManagerInterface as MessageManagerInterface; +use Magento\Framework\Phrase; class ValidationAggregateError extends Exception { @@ -25,7 +26,7 @@ public function __construct(MessageManagerInterface $messageManager) } /** - * @param string|\Magento\Framework\Phrase $error + * @param string|Phrase|Exception $error */ public function addError($error): void { @@ -39,10 +40,26 @@ public function getErrors(): array public function flush(): void { - foreach ($this->errors as $error) { - $this->messageManager->addErrorMessage($error); + foreach ($this->getErrorMessages() as $errorMessage) { + $this->messageManager->addErrorMessage($errorMessage); } $this->errors = []; } + + public function getErrorMessages(): array + { + $errorMessages = []; + foreach ($this->errors as $error) { + if (is_string($error) || $error instanceof Phrase) { + $errorMessages[] = $error; + } + + if ($error instanceof Exception) { + $errorMessages[] = $error->getMessage(); + } + } + + return $errorMessages; + } } diff --git a/Model/ImportExport/Processor/Import/Validator/ValidationException.php b/Model/ImportExport/Processor/Import/Validator/ValidationException.php new file mode 100644 index 00000000..0db8ab54 --- /dev/null +++ b/Model/ImportExport/Processor/Import/Validator/ValidationException.php @@ -0,0 +1,26 @@ +invalidNodePath = $invalidNode; + } + + public function getInvalidNodePath(): array + { + return $this->invalidNodePath; + } +} diff --git a/Model/Menu/Node/Validator.php b/Model/Menu/Node/Validator.php index ff626811..c6962a6f 100644 --- a/Model/Menu/Node/Validator.php +++ b/Model/Menu/Node/Validator.php @@ -4,6 +4,7 @@ namespace Snowdog\Menu\Model\Menu\Node; +use Snowdog\Menu\Model\Menu\Node\Validator\CategoryChild as CategoryChildValidator; use Snowdog\Menu\Model\Menu\Node\Validator\Product as ProductValidator; class Validator @@ -13,9 +14,17 @@ class Validator */ private $product; - public function __construct(ProductValidator $product) - { + /** + * @var CategoryChildValidator + */ + private $categoryChildValidator; + + public function __construct( + ProductValidator $product, + CategoryChildValidator $categoryChildValidator + ) { $this->product = $product; + $this->categoryChildValidator = $categoryChildValidator; } public function validate(array $node): void @@ -24,6 +33,9 @@ public function validate(array $node): void case 'product': $this->product->validate($node); break; + case 'category_child': + $this->categoryChildValidator->validate($node); + break; } } } diff --git a/Model/Menu/Node/Validator/CategoryChild.php b/Model/Menu/Node/Validator/CategoryChild.php new file mode 100644 index 00000000..19145f8e --- /dev/null +++ b/Model/Menu/Node/Validator/CategoryChild.php @@ -0,0 +1,51 @@ +catalog = $catalog; + } + + /** + * @throws ValidatorException + */ + public function validate(array $node) + { + $this->validateParentCategoryField($node); + } + + /** + * @throws ValidatorException + */ + private function validateParentCategoryField(array $node) + { + if (!isset($node['content']) || $node['content'] == '') { + throw new ValidatorException(__('%1 parent category is required.', $this->getNodeTitle($node))); + } + + $childCategoryId = $node['content']; + $isValid = $this->catalog->getCategory($childCategoryId); + if (!$isValid) { + throw new ValidatorException(__('%1 parent category is invalid.', $this->getNodeTitle($node))); + } + } + + private function getNodeTitle($node): string + { + return isset($node['title']) && $node['title'] !== '' + ? 'Node "' . $node['title'] . '"' : 'A node'; + } +} diff --git a/Model/Menu/NodeRepository.php b/Model/Menu/NodeRepository.php index 72ca9bc5..ee3c2cd8 100644 --- a/Model/Menu/NodeRepository.php +++ b/Model/Menu/NodeRepository.php @@ -27,7 +27,7 @@ class NodeRepository implements NodeRepositoryInterface * @var SearchResultsInterfaceFactory */ protected $searchResultsFactory; - + /** * @var CollectionFactory */ @@ -51,7 +51,7 @@ public function save(NodeInterface $object) try { $object->save(); } catch (Exception $e) { - throw new CouldNotSaveException($e->getMessage()); + throw new CouldNotSaveException(__($e->getMessage())); } return $object; } diff --git a/Model/MenuRepository.php b/Model/MenuRepository.php index 1985330a..58b6818f 100644 --- a/Model/MenuRepository.php +++ b/Model/MenuRepository.php @@ -54,7 +54,7 @@ public function save(MenuInterface $menu) try { $this->menuResourceModel->save($menu); } catch (\Exception $e) { - throw new CouldNotSaveException($e->getMessage()); + throw new CouldNotSaveException(__($e->getMessage())); } return $menu; } diff --git a/Model/ResourceModel/NodeType/Product.php b/Model/ResourceModel/NodeType/Product.php index 5e04dbed..d1334aa8 100644 --- a/Model/ResourceModel/NodeType/Product.php +++ b/Model/ResourceModel/NodeType/Product.php @@ -106,7 +106,7 @@ public function fetchConfigData() public function fetchTitleData($storeId = Store::DEFAULT_STORE_ID, $productIds = []) { $collection = $this->productCollection->create(); - $collection->addAttributeToSelect(['name']) + $collection->addAttributeToSelect(['name', 'left']) ->addFieldToFilter('entity_id', ['in' => $productIds]) ->addStoreFilter($storeId); diff --git a/etc/config.xml b/etc/config.xml index f1079057..b8246cd4 100644 --- a/etc/config.xml +++ b/etc/config.xml @@ -10,5 +10,10 @@ + + + 1 + + diff --git a/view/adminhtml/web/vue/field-type/autocomplete-lazy.vue b/view/adminhtml/web/vue/field-type/autocomplete-lazy.vue index 5be9bfd2..e4e7a7d1 100644 --- a/view/adminhtml/web/vue/field-type/autocomplete-lazy.vue +++ b/view/adminhtml/web/vue/field-type/autocomplete-lazy.vue @@ -1,7 +1,14 @@