diff --git a/composer.json b/composer.json index 036367629..063e1babe 100644 --- a/composer.json +++ b/composer.json @@ -25,13 +25,13 @@ } ], "require": { - "magento/framework": "*", - "magento/module-store": "*", - "magento/module-backend": "*", - "magento/module-catalog": "*", - "magento/module-catalog-search": "*", + "magento/framework": ">=100.1.0", + "magento/module-store": ">=100.1.0", + "magento/module-backend": ">=100.1.0", + "magento/module-catalog": ">=100.1.0", + "magento/module-catalog-search": ">=100.1.0", "magento/magento-composer-installer": "*", - "elasticsearch/elasticsearch": "2.1.*" + "elasticsearch/elasticsearch": "^2.1" }, "replace": { "smile/module-elasticsuite-core": "self.version", diff --git a/src/module-elasticsuite-catalog-rule/Model/Rule/Condition/Product/AttributeList.php b/src/module-elasticsuite-catalog-rule/Model/Rule/Condition/Product/AttributeList.php index 0f4dd1dad..69c6d3d0c 100644 --- a/src/module-elasticsuite-catalog-rule/Model/Rule/Condition/Product/AttributeList.php +++ b/src/module-elasticsuite-catalog-rule/Model/Rule/Condition/Product/AttributeList.php @@ -105,10 +105,17 @@ public function getAttributeCollection() if ($this->attributeCollection === null) { $this->attributeCollection = $this->attributeCollectionFactory->create(); - $mapping = $this->getMapping(); + $mapping = $this->getMapping(); + $attributeNameMapping = array_flip($this->fieldNameMapping); - $arrayNameCb = function (FieldInterface $field) { - return $field->getName(); + $arrayNameCb = function (FieldInterface $field) use ($attributeNameMapping) { + $attributeName = $field->getName(); + + if (isset($attributeNameMapping[$attributeName])) { + $attributeName = $attributeNameMapping[$attributeName]; + } + + return $attributeName; }; $attributeFilterCb = function (FieldInterface $field) use ($mapping) { diff --git a/src/module-elasticsuite-catalog-rule/Model/Rule/Condition/Product/NestedFilterInterface.php b/src/module-elasticsuite-catalog-rule/Model/Rule/Condition/Product/NestedFilterInterface.php new file mode 100644 index 000000000..b85b9ab0e --- /dev/null +++ b/src/module-elasticsuite-catalog-rule/Model/Rule/Condition/Product/NestedFilterInterface.php @@ -0,0 +1,32 @@ + + * @copyright 2016 Smile + * @license Open Software License ("OSL") v. 3.0 + */ +namespace Smile\ElasticsuiteCatalogRule\Model\Rule\Condition\Product; + +use Smile\ElasticsuiteCore\Search\Request\QueryInterface; + +/** + * Allow to apply automatic filters on nested field during rule query conversion. + * + * @category Smile + * @package Smile\ElasticsuiteCatalogRule + * @author Aurelien FOUCRET + */ +interface NestedFilterInterface +{ + /** + * @return \Smile\ElasticsuiteCore\Search\Request\QueryInterface + */ + public function getFilter(); +} diff --git a/src/module-elasticsuite-catalog-rule/Model/Rule/Condition/Product/QueryBuilder.php b/src/module-elasticsuite-catalog-rule/Model/Rule/Condition/Product/QueryBuilder.php index 1e304bebd..8139cdd2f 100644 --- a/src/module-elasticsuite-catalog-rule/Model/Rule/Condition/Product/QueryBuilder.php +++ b/src/module-elasticsuite-catalog-rule/Model/Rule/Condition/Product/QueryBuilder.php @@ -38,16 +38,23 @@ class QueryBuilder */ private $attributeList; + /** + * @var NestedFilterInterface[] + */ + private $nestedFilters; + /** * Constructor. * - * @param AttributeList $attributeList Search rule product attributes list - * @param QueryFactory $queryFactory Search query factory. + * @param AttributeList $attributeList Search rule product attributes list + * @param QueryFactory $queryFactory Search query factory. + * @param NestedFilterInterface[] $nestedFilters Filters applied to nested fields during query building. */ - public function __construct(AttributeList $attributeList, QueryFactory $queryFactory) + public function __construct(AttributeList $attributeList, QueryFactory $queryFactory, $nestedFilters = []) { $this->queryFactory = $queryFactory; $this->attributeList = $attributeList; + $this->nestedFilters = $nestedFilters; } /** @@ -59,7 +66,7 @@ public function __construct(AttributeList $attributeList, QueryFactory $queryFac */ public function getSearchQuery(ProductCondition $productCondition) { - $query = null; + $query = null; $this->prepareFieldValue($productCondition); @@ -84,7 +91,19 @@ public function getSearchQuery(ProductCondition $productCondition) $field = $this->getSearchField($productCondition); if ($field->isNested()) { - $nestedQueryParams = ['query' => $query, 'path' => $field->getNestedPath()]; + $nestedPath = $field->getNestedPath(); + $nestedQueryParams = ['query' => $query, 'path' => $nestedPath]; + + if (isset($this->nestedFilters[$nestedPath])) { + $nestedFilterClauses = []; + $nestedFilterClauses['must'][] = $this->nestedFilters[$nestedPath]->getFilter(); + $nestedFilterClauses['must'][] = $nestedQueryParams['query']; + + $nestedFilter = $this->queryFactory->create(QueryInterface::TYPE_BOOL, $nestedFilterClauses); + + $nestedQueryParams['query'] = $nestedFilter; + } + $query = $this->queryFactory->create(QueryInterface::TYPE_NESTED, $nestedQueryParams); } } diff --git a/src/module-elasticsuite-catalog-rule/composer.json b/src/module-elasticsuite-catalog-rule/composer.json index 6d883ebce..3c9f92256 100644 --- a/src/module-elasticsuite-catalog-rule/composer.json +++ b/src/module-elasticsuite-catalog-rule/composer.json @@ -20,7 +20,7 @@ ], "require": { "php": "~5.5.0|~5.6.0|~7.0.0", - "magento/framework": "*", + "magento/framework": ">=100.1.0", "smile/module-elasticsuite-catalog": "self.version" }, "version": "2.1.0", diff --git a/src/module-elasticsuite-catalog-rule/view/adminhtml/templates/product/conditions.phtml b/src/module-elasticsuite-catalog-rule/view/adminhtml/templates/product/conditions.phtml index acfd54936..9fd275ad7 100644 --- a/src/module-elasticsuite-catalog-rule/view/adminhtml/templates/product/conditions.phtml +++ b/src/module-elasticsuite-catalog-rule/view/adminhtml/templates/product/conditions.phtml @@ -1,4 +1,25 @@ + * @copyright 2016 Smile + * @license Open Software License ("OSL") v. 3.0 + * + * Product search rule admin form rendering. + */ +?> + +getElement(); $fieldId = ($element->getHtmlContainerId()) ? ' id="' . $element->getHtmlContainerId() . '"' : ''; $fieldClass = "field admin__field field-{$element->getId()} {$element->getCssClass()}"; @@ -17,12 +38,11 @@ $fieldAttributes = $fieldId . ' class="' . $fieldClass . '" ' . $block->getUiId( - diff --git a/src/module-elasticsuite-catalog-rule/view/adminhtml/web/css/source/_module.less b/src/module-elasticsuite-catalog-rule/view/adminhtml/web/css/source/_module.less new file mode 100644 index 000000000..77842731b --- /dev/null +++ b/src/module-elasticsuite-catalog-rule/view/adminhtml/web/css/source/_module.less @@ -0,0 +1,18 @@ +// /** +// * DISCLAIMER +// * +// * Do not edit or add to this file if you wish to upgrade Smile Elastic Suite to newer +// * versions in the future. +// * +// * +// * @category Smile +// * @package Smile\ElasticsuiteCatalogRule +// * @author Aurelien FOUCRET +// * @copyright 2016 Smile +// * @license Open Software License ("OSL") v. 3.0 +// */ + +// Display fieldsets legend into catalog category merchandising tab. +.admin__fieldset.virtual-rule-fieldset { + padding: 0 +} diff --git a/src/module-elasticsuite-catalog-rule/view/adminhtml/web/js/component/catalog/product/form/rule.js b/src/module-elasticsuite-catalog-rule/view/adminhtml/web/js/component/catalog/product/form/rule.js new file mode 100644 index 000000000..775b4417f --- /dev/null +++ b/src/module-elasticsuite-catalog-rule/view/adminhtml/web/js/component/catalog/product/form/rule.js @@ -0,0 +1,96 @@ +/** + * DISCLAIMER + * + * Do not edit or add to this file if you wish to upgrade Smile Elastic Suite to newer + * versions in the future. + * + * + * @category Smile + * @package Smile\ElasticsuiteCatalogRule + * @author Aurelien FOUCRET + * @copyright 2016 Smile + * @license Open Software License ("OSL") v. 3.0 + */ + +define([ + 'Magento_Ui/js/form/components/html', + 'jquery', + 'MutationObserver' +], function (Component, $) { + 'use strict'; + + return Component.extend({ + defaults: { + value: {}, + links: { + value: '${ $.provider }:${ $.dataScope }' + }, + additionalClasses: "admin__fieldset virtual-rule-fieldset" + }, + initialize: function () { + this._super(); + this.initRuleListener(); + }, + + initObservable: function () { + this._super(); + this.ruleObject = {}; + this.observe('ruleObject value'); + + return this; + }, + + initRuleListener: function () { + var observer = new MutationObserver(function () { + var rootNode = document.getElementById(this.index); + if (rootNode !== null) { + this.rootNode = document.getElementById(this.index); + observer.disconnect(); + var ruleObserver = new MutationObserver(this.updateRule.bind(this)); + var ruleObserverConfig = {childList:true, subtree: true, attributes: true}; + ruleObserver.observe(rootNode, ruleObserverConfig); + this.updateRule(); + } + }.bind(this)); + var observerConfig = {childList: true, subtree: true}; + observer.observe(document, observerConfig) + }, + + updateRule: function () { + var ruleObject = {}; + var hashValues = []; + + $(this.rootNode).find("[name*=" + this.index + "]").each(function () { + hashValues.push(this.name + this.value.toString()); + var currentRuleObject = ruleObject; + + var path = this.name.match(/\[([^[\[\]]+)\]/g) + .map(function (pathItem) { return pathItem.substr(1, pathItem.length-2); }); + + while (path.length > 1) { + var currentKey = path.shift(); + + if (currentRuleObject[currentKey] === undefined) { + currentRuleObject[currentKey] = {}; + } + + currentRuleObject = currentRuleObject[currentKey]; + } + + currentKey = path.shift(); + currentRuleObject[currentKey] = $(this).val(); + }); + + var newHashValue = hashValues.sort().join(''); + + if (newHashValue !== this.currentHashValue) { + if (this.currentHashValue !== undefined) { + this.bubble('update', true); + } + this.currentHashValue = newHashValue; + this.ruleObject(ruleObject); + this.value(ruleObject); + } + } + }) +}); diff --git a/src/module-elasticsuite-catalog/Block/Adminhtml/Catalog/Product/Form/Renderer/Sort.php b/src/module-elasticsuite-catalog/Block/Adminhtml/Catalog/Product/Form/Renderer/Sort.php deleted file mode 100644 index 19a1e00f9..000000000 --- a/src/module-elasticsuite-catalog/Block/Adminhtml/Catalog/Product/Form/Renderer/Sort.php +++ /dev/null @@ -1,96 +0,0 @@ - - * @copyright 2016 Smile - * @license Open Software License ("OSL") v. 3.0 - */ - -namespace Smile\ElasticsuiteCatalog\Block\Adminhtml\Catalog\Product\Form\Renderer; - -use Magento\Backend\Block\Template; -use Magento\Framework\Locale\FormatInterface; -use Magento\Framework\Data\Form\Element\AbstractElement; -use Magento\Framework\Data\Form\Element\Renderer\RendererInterface; - -/** - * A generic block to allow admin having a nice product sorter with preview and drag and drop feature. - * - * @SuppressWarnings(PHPMD.CamelCasePropertyName) - * - * @category Smile - * @package Smile\ElasticsuiteCatalog - * @author Aurelien FOUCRET - */ -class Sort extends Template implements RendererInterface -{ - /** - * @var string - */ - const JS_COMPONENT = 'Smile_ElasticsuiteCatalog/js/catalog/product/form/renderer/sort'; - - /** - * @var string - */ - const JS_TEMPLATE = 'Smile_ElasticsuiteCatalog/catalog/product/form/renderer/sort'; - - /** - * @var string - */ - protected $_template = 'catalog/product/form/renderer/sort.phtml'; - - /** - * @var \Magento\Framework\Locale\FormatInterface - */ - private $localeFormat; - - /** - * Constructor. - * - * @param \Magento\Backend\Block\Template\Context $context Template context. - * @param \Magento\Framework\Locale\FormatInterface $localeFormat Locale format. - * @param array $data Additional data. - */ - public function __construct(\Magento\Backend\Block\Template\Context $context, FormatInterface $localeFormat, array $data = []) - { - parent::__construct($context, $data); - $this->localeFormat = $localeFormat; - } - - /** - * {@inheritDoc} - */ - public function render(\Magento\Framework\Data\Form\Element\AbstractElement $element) - { - $this->setElement($element); - - return $this->toHtml(); - } - - /** - * {@inheritDoc} - */ - public function getJsLayout() - { - $layoutJsComponents = []; - $layoutJsComponents['adminProductSort']['component'] = self::JS_COMPONENT; - $layoutJsComponents['adminProductSort']['config'] = [ - 'template' => self::JS_TEMPLATE, - 'loadUrl' => $this->getElement()->getLoadUrl(), - 'targetElementName' => $this->getElement()->getName(), - 'formId' => $this->getElement()->getFormId(), - 'refreshElements' => $this->getElement()->getRefreshElements(), - 'savedPositions' => $this->getElement()->getSavedPositions(), - 'pageSize' => $this->getElement()->getPageSize(), - 'priceFormat' => $this->localeFormat->getPriceFormat(), - ]; - - return json_encode(['components' => $layoutJsComponents]); - } -} diff --git a/src/module-elasticsuite-catalog/Block/Navigation.php b/src/module-elasticsuite-catalog/Block/Navigation.php index 8f65dfacf..4a795ab12 100644 --- a/src/module-elasticsuite-catalog/Block/Navigation.php +++ b/src/module-elasticsuite-catalog/Block/Navigation.php @@ -14,6 +14,13 @@ namespace Smile\ElasticsuiteCatalog\Block; +use Magento\Catalog\Model\Layer\AvailabilityFlagInterface; +use Magento\Catalog\Model\Layer\FilterList; +use Magento\Catalog\Model\Layer\Resolver; +use Magento\Framework\Module\Manager; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\View\Element\Template\Context; + /** * Custom implementation of the navigation block to apply facet coverage rate. * @@ -23,6 +30,64 @@ */ class Navigation extends \Magento\LayeredNavigation\Block\Navigation { + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var Manager + */ + private $moduleManager; + + /** + * Navigation constructor. + * + * @param \Magento\Framework\View\Element\Template\Context $context Application context + * @param \Magento\Catalog\Model\Layer\Resolver $layerResolver Layer Resolver + * @param \Magento\Catalog\Model\Layer\FilterList $filterList Filter List + * @param \Magento\Catalog\Model\Layer\AvailabilityFlagInterface $visibilityFlag Visibility Flag + * @param \Magento\Framework\ObjectManagerInterface $objectManager Object Manager + * @param \Magento\Framework\Module\Manager $moduleManager Module Manager + * @param array $data Block Data + */ + public function __construct( + Context $context, + Resolver $layerResolver, + FilterList $filterList, + AvailabilityFlagInterface $visibilityFlag, + ObjectManagerInterface $objectManager, + Manager $moduleManager, + array $data + ) { + $this->objectManager = $objectManager; + $this->moduleManager = $moduleManager; + + parent::__construct($context, $layerResolver, $filterList, $visibilityFlag, $data); + } + + /** + * Check if we can show this block. + * According to @see \Magento\LayeredNavigationStaging\Block\Navigation::canShowBlock + * We should not show the block if staging is enabled and if we are currently previewing the results. + * + * @return bool + */ + public function canShowBlock() + { + if ($this->moduleManager->isEnabled('Magento_Staging')) { + try { + $versionManager = $this->objectManager->get('\Magento\Staging\Model\VersionManager'); + + return parent::canShowBlock() && !$versionManager->isPreviewVersion(); + } catch (\Exception $exception) { + return parent::canShowBlock(); + } + } + + return parent::canShowBlock(); + } + /** * @SuppressWarnings(PHPMD.CamelCaseMethodName) * diff --git a/src/module-elasticsuite-catalog/Block/Navigation/Renderer/PriceSlider.php b/src/module-elasticsuite-catalog/Block/Navigation/Renderer/PriceSlider.php index f77b45d08..5d8ddb023 100644 --- a/src/module-elasticsuite-catalog/Block/Navigation/Renderer/PriceSlider.php +++ b/src/module-elasticsuite-catalog/Block/Navigation/Renderer/PriceSlider.php @@ -12,7 +12,9 @@ */ namespace Smile\ElasticsuiteCatalog\Block\Navigation\Renderer; +use Magento\Store\Model\ScopeInterface; use Smile\ElasticsuiteCatalog\Model\Layer\Filter\Price; +use Magento\Catalog\Model\Layer\Filter\DataProvider\Price as PriceDataProvider; /** * This block handle price slider rendering. @@ -45,4 +47,81 @@ protected function getFieldFormat() { return $this->localeFormat->getPriceFormat(); } + + /** + * {@inheritDoc} + */ + protected function getConfig() + { + $config = parent::getConfig(); + + if ($this->isManualCalculation() && ($this->getStepValue() > 0)) { + $config['step'] = $this->getStepValue(); + } + + return $config; + } + + /** + * Returns min value of the slider. + * + * @return int + */ + protected function getMinValue() + { + $minValue = $this->getFilter()->getMinValue(); + + if ($this->isManualCalculation() && ($this->getStepValue() > 0)) { + $stepValue = $this->getStepValue(); + $minValue = floor($minValue / $stepValue) * $stepValue; + } + + return $minValue; + } + + /** + * Returns max value of the slider. + * + * @return int + */ + protected function getMaxValue() + { + $maxValue = $this->getFilter()->getMaxValue() + 1; + + if ($this->isManualCalculation() && ($this->getStepValue() > 0)) { + $stepValue = $this->getStepValue(); + $maxValue = ceil($maxValue / $stepValue) * $stepValue; + } + + return $maxValue; + } + + /** + * Check if price interval is manually set in the configuration + * + * @return bool + */ + private function isManualCalculation() + { + $result = false; + $calculation = $this->_scopeConfig->getValue(PriceDataProvider::XML_PATH_RANGE_CALCULATION, ScopeInterface::SCOPE_STORE); + + if ($calculation === PriceDataProvider::RANGE_CALCULATION_MANUAL) { + $result = true; + } + + return $result; + } + + /** + * Retrieve the value for "Default Price Navigation Step". + * + * @return int + */ + private function getStepValue() + { + $value = $this->_scopeConfig->getValue(PriceDataProvider::XML_PATH_RANGE_STEP, ScopeInterface::SCOPE_STORE); + + return (int) $value; + } } diff --git a/src/module-elasticsuite-catalog/Block/Navigation/Renderer/Slider.php b/src/module-elasticsuite-catalog/Block/Navigation/Renderer/Slider.php index 6bc150e43..e6f449920 100644 --- a/src/module-elasticsuite-catalog/Block/Navigation/Renderer/Slider.php +++ b/src/module-elasticsuite-catalog/Block/Navigation/Renderer/Slider.php @@ -73,18 +73,7 @@ public function __construct( */ public function getJsonConfig() { - $config = [ - 'minValue' => $this->getMinValue(), - 'maxValue' => $this->getMaxValue(), - 'currentValue' => $this->getCurrentValue(), - 'fieldFormat' => $this->getFieldFormat(), - 'intervals' => $this->getIntervals(), - 'urlTemplate' => $this->getUrlTemplate(), - 'messageTemplates' => [ - 'displayCount' => __('<%- count %> products'), - 'displayEmpty' => __('No products in the selected range.'), - ], - ]; + $config = $this->getConfig(); return $this->jsonEncoder->encode($config); } @@ -128,12 +117,35 @@ protected function getFieldFormat() return $format; } + /** + * Retrieve configuration + * + * @return array + */ + protected function getConfig() + { + $config = [ + 'minValue' => $this->getMinValue(), + 'maxValue' => $this->getMaxValue(), + 'currentValue' => $this->getCurrentValue(), + 'fieldFormat' => $this->getFieldFormat(), + 'intervals' => $this->getIntervals(), + 'urlTemplate' => $this->getUrlTemplate(), + 'messageTemplates' => [ + 'displayCount' => __('<%- count %> products'), + 'displayEmpty' => __('No products in the selected range.'), + ], + ]; + + return $config; + } + /** * Returns min value of the slider. * * @return int */ - private function getMinValue() + protected function getMinValue() { return $this->getFilter()->getMinValue(); } @@ -143,7 +155,7 @@ private function getMinValue() * * @return int */ - private function getMaxValue() + protected function getMaxValue() { return $this->getFilter()->getMaxValue() + 1; } diff --git a/src/module-elasticsuite-catalog/Helper/Attribute.php b/src/module-elasticsuite-catalog/Helper/Attribute.php index 99323c0fa..4d658bd11 100644 --- a/src/module-elasticsuite-catalog/Helper/Attribute.php +++ b/src/module-elasticsuite-catalog/Helper/Attribute.php @@ -171,11 +171,17 @@ public function prepareIndexValue(AttributeInterface $attribute, $storeId, $valu $value = [$value]; } - $values[$attributeCode] = $value = array_filter(array_map($simpleValueMapper, $value)); + $value = array_map($simpleValueMapper, $value); + $value = array_filter($value); + $value = array_values($value); + $values[$attributeCode] = $value; if ($attribute->usesSource()) { $optionTextFieldName = $this->getOptionTextFieldName($attributeCode); - $values[$optionTextFieldName] = array_filter($this->getIndexOptionsText($attribute, $storeId, $value)); + $optionTextValues = $this->getIndexOptionsText($attribute, $storeId, $value); + $optionTextValues = array_filter($optionTextValues); + $optionTextValues = array_values($optionTextValues); + $values[$optionTextFieldName] = $optionTextValues; } return array_filter($values); diff --git a/src/module-elasticsuite-catalog/Model/Autocomplete/Category/DataProvider.php b/src/module-elasticsuite-catalog/Model/Autocomplete/Category/DataProvider.php new file mode 100644 index 000000000..4421a1379 --- /dev/null +++ b/src/module-elasticsuite-catalog/Model/Autocomplete/Category/DataProvider.php @@ -0,0 +1,168 @@ + + * @copyright 2016 Smile + * @license Open Software License ("OSL") v. 3.0 + */ +namespace Smile\ElasticsuiteCatalog\Model\Autocomplete\Category; + +use Magento\Search\Model\Autocomplete\DataProviderInterface; +use Magento\Search\Model\QueryFactory; +use Smile\ElasticsuiteCatalog\Helper\Autocomplete as ConfigurationHelper; +use Smile\ElasticsuiteCatalog\Model\ResourceModel\Category\Fulltext\CollectionFactory as CategoryCollectionFactory; +use Smile\ElasticsuiteCore\Model\Autocomplete\Terms\DataProvider as TermDataProvider; + +/** + * Catalog category autocomplete data provider. + * + * @category Smile + * @package Smile\ElasticSuiteCatalog + * @author Romain Ruaud + */ +class DataProvider implements DataProviderInterface +{ + /** + * Autocomplete type + */ + const AUTOCOMPLETE_TYPE = "category"; + + /** + * Autocomplete result item factory + * + * @var ItemFactory + */ + protected $itemFactory; + + /** + * Query factory + * + * @var QueryFactory + */ + protected $queryFactory; + + /** + * @var TermDataProvider + */ + protected $termDataProvider; + + /** + * @var CategoryCollectionFactory + */ + protected $categoryCollectionFactory; + + /** + * @var ConfigurationHelper + */ + protected $configurationHelper; + + /** + * @var string Autocomplete result type + */ + private $type; + + /** + * Constructor. + * + * @param ItemFactory $itemFactory Suggest item factory. + * @param QueryFactory $queryFactory Search query factory. + * @param TermDataProvider $termDataProvider Search terms suggester. + * @param CategoryCollectionFactory $categoryCollectionFactory Category collection factory. + * @param ConfigurationHelper $configurationHelper Autocomplete configuration helper. + * @param string $type Autocomplete provider type. + */ + public function __construct( + ItemFactory $itemFactory, + QueryFactory $queryFactory, + TermDataProvider $termDataProvider, + CategoryCollectionFactory $categoryCollectionFactory, + ConfigurationHelper $configurationHelper, + $type = self::AUTOCOMPLETE_TYPE + ) { + $this->itemFactory = $itemFactory; + $this->queryFactory = $queryFactory; + $this->termDataProvider = $termDataProvider; + $this->categoryCollectionFactory = $categoryCollectionFactory; + $this->configurationHelper = $configurationHelper; + $this->type = $type; + } + + /** + * @return string + */ + public function getType() + { + return $this->type; + } + + /** + * {@inheritDoc} + */ + public function getItems() + { + $result = []; + $categoryCollection = $this->getCategoryCollection(); + if ($categoryCollection) { + foreach ($categoryCollection as $category) { + $result[] = $this->itemFactory->create(['category' => $category, 'type' => $this->getType()]); + } + } + + return $result; + } + + /** + * List of search terms suggested by the search terms data provider. + * + * @return array + */ + private function getSuggestedTerms() + { + $terms = array_map( + function (\Magento\Search\Model\Autocomplete\Item $termItem) { + return $termItem->getTitle(); + }, + $this->termDataProvider->getItems() + ); + + return $terms; + } + + /** + * Suggested categories collection. + * Returns null if no suggested search terms. + * + * @return \Smile\ElasticsuiteCatalog\Model\ResourceModel\Category\Fulltext\Collection|null + */ + private function getCategoryCollection() + { + $categoryCollection = null; + $suggestedTerms = $this->getSuggestedTerms(); + $terms = [$this->queryFactory->get()->getQueryText()]; + + if (!empty($suggestedTerms)) { + $terms = array_merge($terms, $suggestedTerms); + } + + $categoryCollection = $this->categoryCollectionFactory->create(); + $categoryCollection->addSearchFilter($terms); + $categoryCollection->setPageSize($this->getResultsPageSize()); + + return $categoryCollection; + } + + /** + * Retrieve number of categories to display in autocomplete results + * + * @return int + */ + private function getResultsPageSize() + { + return $this->configurationHelper->getMaxSize($this->getType()); + } +} diff --git a/src/module-elasticsuite-catalog/Model/Autocomplete/Category/ItemFactory.php b/src/module-elasticsuite-catalog/Model/Autocomplete/Category/ItemFactory.php new file mode 100644 index 000000000..0718be170 --- /dev/null +++ b/src/module-elasticsuite-catalog/Model/Autocomplete/Category/ItemFactory.php @@ -0,0 +1,225 @@ + + * @copyright 2016 Smile + * @license Open Software License ("OSL") v. 3.0 + */ + +namespace Smile\ElasticsuiteCatalog\Model\Autocomplete\Category; + +use Magento\Catalog\Model\CategoryFactory; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\UrlInterface; + +/** + * Create an autocomplete item from a category. + * + * @category Smile + * @package Smile\ElasticsuiteCatalog + * @author Romain Ruaud + */ +class ItemFactory extends \Magento\Search\Model\Autocomplete\ItemFactory +{ + /** + * XML path for category url suffix + */ + const XML_PATH_CATEGORY_URL_SUFFIX = 'catalog/seo/category_url_suffix'; + + /** + * The offset to display on the beginning of the Breadcrumb + */ + const START_BREADCRUMB_OFFSET = 1; + + /** + * The offset to display on the end of the Breadcrumb + */ + const END_BREADCRUMB_OFFSET = 1; + + /** + * The string used when chunking + */ + const CHUNK_STRING = "..."; + + /** + * @var array An array containing category names, to use as local cache + */ + protected $categoryNames = []; + + /** + * @var \Magento\Framework\UrlInterface + */ + private $urlBuilder; + + /** + * @var null + */ + private $categoryUrlSuffix = null; + + /** + * @var \Magento\Catalog\Model\CategoryFactory|null + */ + private $categoryFactory = null; + + /** + * ItemFactory constructor. + * + * @param ObjectManagerInterface $objectManager The Object Manager + * @param UrlInterface $urlBuilder The Url Builder + * @param ScopeConfigInterface $scopeConfig The Scope Config + * @param CategoryFactory $categoryFactory Category Factory + */ + public function __construct( + ObjectManagerInterface $objectManager, + UrlInterface $urlBuilder, + ScopeConfigInterface $scopeConfig, + CategoryFactory $categoryFactory + ) { + parent::__construct($objectManager); + $this->urlBuilder = $urlBuilder; + $this->categoryUrlSuffix = $scopeConfig->getValue(self::XML_PATH_CATEGORY_URL_SUFFIX); + $this->categoryFactory = $categoryFactory; + } + + /** + * {@inheritDoc} + */ + public function create(array $data) + { + $data = $this->addCategoryData($data); + unset($data['category']); + + return parent::create($data); + } + + /** + * Load category data and append them to the original data. + * + * @param array $data Autocomplete item data. + * + * @return array + */ + private function addCategoryData($data) + { + $category = $data['category']; + + $documentSource = $category->getDocumentSource(); + + $categoryData = [ + 'title' => $documentSource['name'], + 'url' => $this->getCategoryUrl($category), + 'breadcrumb' => $this->getCategoryBreadcrumb($category), + ]; + + $data = array_merge($data, $categoryData); + + return $data; + } + + /** + * Retrieve category Url from the document source. + * Done from the document source to prevent having to use addUrlRewrite to result on category collection. + * + * @param \Magento\Catalog\Model\Category $category The category. + * + * @return string + */ + private function getCategoryUrl($category) + { + $documentSource = $category->getDocumentSource(); + + if ($documentSource && isset($documentSource['url_path'])) { + $urlPath = is_array($documentSource['url_path']) ? current($documentSource['url_path']) : $documentSource['url_path']; + + $url = trim($this->urlBuilder->getUrl($urlPath), '/') . $this->categoryUrlSuffix; + + return $url; + } + + return $category->getUrl(); + } + + /** + * Return a mini-breadcrumb for a category + * + * @param \Magento\Catalog\Model\Category $category The category + * + * @return array + */ + private function getCategoryBreadcrumb(\Magento\Catalog\Model\Category $category) + { + $chunkPath = $this->getChunkedPath($category); + $breadcrumb = []; + + foreach ($chunkPath as $categoryId) { + $breadcrumb[] = $this->getCategoryNameById($categoryId, $category->getStoreId()); + } + + return implode(' > ', $breadcrumb); + } + + /** + * Return chunked (if needed) path for a category + * + * A chunked path is the first 2 highest ancestors and the 2 lowests levels of path + * + * If path is not longer than 4, complete path is used + * + * @param \Magento\Catalog\Model\Category $category The category + * + * @return array + */ + private function getChunkedPath(\Magento\Catalog\Model\Category $category) + { + $path = $category->getPath(); + $rawPath = explode('/', $path); + + // First occurence is root category (1), second is root category of store. + $rawPath = array_slice($rawPath, 2); + + // Last occurence is the category displayed. + array_pop($rawPath); + + $chunkedPath = $rawPath; + + if (count($rawPath) > (self::START_BREADCRUMB_OFFSET + self::END_BREADCRUMB_OFFSET)) { + $chunkedPath = array_merge( + array_slice($rawPath, 0, self::START_BREADCRUMB_OFFSET), + [self::CHUNK_STRING], + array_slice($rawPath, -self::END_BREADCRUMB_OFFSET) + ); + } + + return $chunkedPath; + } + + /** + * Retrieve a category name by it's id, and store it in local cache + * + * @param int $categoryId The category Id + * @param int $storeId The store Id + * + * @return string + */ + private function getCategoryNameById($categoryId, $storeId) + { + if ($categoryId == self::CHUNK_STRING) { + return self::CHUNK_STRING; + } + + if (!isset($this->categoryNames[$categoryId])) { + $category = $this->categoryFactory->create(); + $categoryResource = $category->getResource(); + $this->categoryNames[$categoryId] = $categoryResource->getAttributeRawValue($categoryId, "name", $storeId); + } + + return $this->categoryNames[$categoryId]; + } +} diff --git a/src/module-elasticsuite-catalog/Model/Autocomplete/Product/DataProvider.php b/src/module-elasticsuite-catalog/Model/Autocomplete/Product/DataProvider.php index faa2b6c25..a0424a562 100644 --- a/src/module-elasticsuite-catalog/Model/Autocomplete/Product/DataProvider.php +++ b/src/module-elasticsuite-catalog/Model/Autocomplete/Product/DataProvider.php @@ -150,7 +150,7 @@ private function getProductCollection() $terms = array_merge($terms, $suggestedTerms); } - $productCollection = $this->productCollectionFactory->create(); + $productCollection = $this->productCollectionFactory->create(['searchRequestName' => 'quick_search_container']); $productCollection->addSearchFilter($terms); $productCollection->setPageSize($this->getResultsPageSize()); $productCollection diff --git a/src/module-elasticsuite-catalog/Model/Layer/Filter/Item/Category.php b/src/module-elasticsuite-catalog/Model/Layer/Filter/Item/Category.php index 980780cec..42ff96148 100644 --- a/src/module-elasticsuite-catalog/Model/Layer/Filter/Item/Category.php +++ b/src/module-elasticsuite-catalog/Model/Layer/Filter/Item/Category.php @@ -27,7 +27,16 @@ class Category extends \Magento\Catalog\Model\Layer\Filter\Item */ public function getUrl() { - $url = parent::getUrl(); + $query = [ + $this->getFilter()->getRequestVar() => $this->getValue(), + $this->_htmlPagerBlock->getPageVarName() => null, + ]; + + foreach ($this->getFilter()->getLayer()->getState()->getFilters() as $currentFilterItem) { + $query[$currentFilterItem->getFilter()->getRequestVar()] = null; + } + + $url = $this->_url->getUrl('*/*/*', ['_current' => true, '_use_rewrite' => true, '_query' => $query]); if ($this->getUrlRewrite()) { $url = $this->getUrlRewrite(); diff --git a/src/module-elasticsuite-catalog/Model/Layer/Filter/Price.php b/src/module-elasticsuite-catalog/Model/Layer/Filter/Price.php index ed58cf1fb..c9c5b35a4 100644 --- a/src/module-elasticsuite-catalog/Model/Layer/Filter/Price.php +++ b/src/module-elasticsuite-catalog/Model/Layer/Filter/Price.php @@ -105,6 +105,13 @@ public function addFacetToCollection() $facetConfig = ['nestedFilter' => ['price.customer_group_id' => $customerGroupId]]; + $calculation = $this->dataProvider->getRangeCalculationValue(); + if ($calculation === \Magento\Catalog\Model\Layer\Filter\DataProvider\Price::RANGE_CALCULATION_MANUAL) { + if ((int) $this->dataProvider->getRangeStepValue() > 0) { + $facetConfig['interval'] = (int) $this->dataProvider->getRangeStepValue(); + } + } + $productCollection = $this->getLayer()->getProductCollection(); $productCollection->addFacet($facetField, $facetType, $facetConfig); diff --git a/src/module-elasticsuite-catalog/Model/Product/Indexer/Fulltext/Datasource/CategoryData.php b/src/module-elasticsuite-catalog/Model/Product/Indexer/Fulltext/Datasource/CategoryData.php index 20d230017..a1b897536 100644 --- a/src/module-elasticsuite-catalog/Model/Product/Indexer/Fulltext/Datasource/CategoryData.php +++ b/src/module-elasticsuite-catalog/Model/Product/Indexer/Fulltext/Datasource/CategoryData.php @@ -52,17 +52,22 @@ public function addData($storeId, array $indexData) foreach ($categoryData as $categoryDataRow) { $productId = (int) $categoryDataRow['product_id']; - $currentCategoryData = [ - 'category_id' => (int) $categoryDataRow['category_id'], - 'is_parent' => (bool) $categoryDataRow['is_parent'], - 'name' => (string) $categoryDataRow['name'], - ]; + unset($categoryDataRow['product_id']); + + $categoryDataRow = array_merge( + $categoryDataRow, + [ + 'category_id' => (int) $categoryDataRow['category_id'], + 'is_parent' => (bool) $categoryDataRow['is_parent'], + 'name' => (string) $categoryDataRow['name'], + ] + ); if (isset($categoryDataRow['position']) && $categoryDataRow['position'] !== null) { - $currentCategoryData['position'] = (int) $categoryDataRow['position']; + $categoryDataRow['position'] = (int) $categoryDataRow['position']; } - $indexData[$productId]['category'][] = $currentCategoryData; + $indexData[$productId]['category'][] = array_filter($categoryDataRow); } return $indexData; diff --git a/src/module-elasticsuite-catalog/Model/ResourceModel/Category/Fulltext/Collection.php b/src/module-elasticsuite-catalog/Model/ResourceModel/Category/Fulltext/Collection.php new file mode 100644 index 000000000..7f29710e4 --- /dev/null +++ b/src/module-elasticsuite-catalog/Model/ResourceModel/Category/Fulltext/Collection.php @@ -0,0 +1,395 @@ + + * @copyright 2016 Smile + * @license Open Software License ("OSL") v. 3.0 + */ +namespace Smile\ElasticsuiteCatalog\Model\ResourceModel\Category\Fulltext; + +use Smile\ElasticsuiteCore\Search\Adapter\Elasticsuite\Response\QueryResponse; +use Smile\ElasticsuiteCore\Search\Request\QueryInterface; +use Smile\ElasticsuiteCore\Search\RequestInterface; + +/** + * Search engine category collection for Autocomplete. + * Basically a copy-pasted version of @see Smile\ElasticsuiteCatalog\Model\ResourceModel\Product\Fulltext\Collection + * + * @codingStandardsIgnoreStart + * @TODO Refactor/Mutualize all copy/pasted methods. + * @codingStandardsIgnoreEnd + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * + * @category Smile + * @package Smile\ElasticSuiteCatalog + * @author Romain Ruaud + */ +class Collection extends \Magento\Catalog\Model\ResourceModel\Category\Collection +{ + /** + * @var \Smile\ElasticsuiteCore\Search\Request\Builder + */ + private $requestBuilder; + + /** + * @var \Magento\Search\Model\SearchEngine + */ + private $searchEngine; + + /** + * @var string + */ + private $searchRequestName; + + /** + * @var array + */ + private $filters = []; + + /** + * @var QueryInterface[] + */ + private $queryFilters = []; + + /** + * @var array + */ + private $facets = []; + + /** + * @var QueryResponse + */ + private $queryResponse; + + /** + * @var string + */ + private $queryText; + + /** + * @var boolean + */ + private $isSpellchecked = false; + + /** + * Collection constructor. + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + * + * @param \Magento\Framework\Data\Collection\EntityFactory $entityFactory The Entity Factory + * @param \Psr\Log\LoggerInterface $logger The Logger + * @param \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy Fetch Strategy + * @param \Magento\Framework\Event\ManagerInterface $eventManager Event Manager + * @param \Magento\Eav\Model\Config $eavConfig EAV Configuration + * @param \Magento\Framework\App\ResourceConnection $resource Resource Connection + * @param \Magento\Eav\Model\EntityFactory $eavEntityFactory Entity Factory + * @param \Magento\Eav\Model\ResourceModel\Helper $resourceHelper Resource Helper + * @param \Magento\Framework\Validator\UniversalFactory $universalFactory Universal Factory + * @param \Magento\Store\Model\StoreManagerInterface $storeManager Store Manager + * @param \Smile\ElasticsuiteCore\Search\Request\Builder $requestBuilder Search request builder. + * @param \Magento\Search\Model\SearchEngine $searchEngine Search engine + * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection Db Connection. + * @param string $searchRequestName Search request name. + */ + public function __construct( + \Magento\Framework\Data\Collection\EntityFactory $entityFactory, + \Psr\Log\LoggerInterface $logger, + \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy, + \Magento\Framework\Event\ManagerInterface $eventManager, + \Magento\Eav\Model\Config $eavConfig, + \Magento\Framework\App\ResourceConnection $resource, + \Magento\Eav\Model\EntityFactory $eavEntityFactory, + \Magento\Eav\Model\ResourceModel\Helper $resourceHelper, + \Magento\Framework\Validator\UniversalFactory $universalFactory, + \Magento\Store\Model\StoreManagerInterface $storeManager, + \Smile\ElasticsuiteCore\Search\Request\Builder $requestBuilder, + \Magento\Search\Model\SearchEngine $searchEngine, + \Magento\Framework\DB\Adapter\AdapterInterface $connection = null, + $searchRequestName = 'category_search_container' + ) { + parent::__construct( + $entityFactory, + $logger, + $fetchStrategy, + $eventManager, + $eavConfig, + $resource, + $eavEntityFactory, + $resourceHelper, + $universalFactory, + $storeManager, + $connection + ); + + $this->requestBuilder = $requestBuilder; + $this->searchEngine = $searchEngine; + $this->searchRequestName = $searchRequestName; + } + + /** + * {@inheritDoc} + */ + public function getSize() + { + if ($this->_totalRecords === null) { + $this->loadItemCounts(); + } + + return $this->_totalRecords; + } + + /** + * {@inheritDoc} + */ + public function setOrder($attribute, $dir = self::SORT_ORDER_DESC) + { + $this->_orders[$attribute] = $dir; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function addFieldToFilter($field, $condition = null) + { + $this->filters[$field] = $condition; + + return $this; + } + + /** + * {@inheritDoc} + */ + public function addAttributeToSort($attribute, $dir = self::SORT_ORDER_ASC) + { + return $this->setOrder($attribute, $dir); + } + + /** + * Append a prebuilt (QueryInterface) query filter to the collection. + * + * @param QueryInterface $queryFilter Query filter. + * + * @return $this + */ + public function addQueryFilter(QueryInterface $queryFilter) + { + $this->queryFilters[] = $queryFilter; + + return $this; + } + + /** + * Add search query filter + * + * @param string $query Search query text. + * + * @return \Smile\ElasticsuiteCatalog\Model\ResourceModel\Product\Fulltext\Collection + */ + public function addSearchFilter($query) + { + $this->queryText = $query; + + return $this; + } + + /** + * Append a facet to the collection + * + * @param string $field Facet field. + * @param string $facetType Facet type. + * @param array $facetConfig Facet config params. + * @param array $facetFilter Facet filter. + * + * @return \Smile\ElasticsuiteCatalog\Model\ResourceModel\Product\Fulltext\Collection + */ + public function addFacet($field, $facetType, $facetConfig, $facetFilter = null) + { + $this->facets[$field] = ['type' => $facetType, 'filter' => $facetFilter, 'config' => $facetConfig]; + + return $this; + } + + /** + * Return field faceted data from faceted search result. + * + * @param string $field Facet field. + * + * @return array + */ + public function getFacetedData($field) + { + $this->_renderFilters(); + $result = []; + $aggregations = $this->queryResponse->getAggregations(); + + $bucket = $aggregations->getBucket($field); + + if ($bucket) { + foreach ($bucket->getValues() as $value) { + $metrics = $value->getMetrics(); + $result[$metrics['value']] = $metrics; + } + } + + return $result; + } + + /** + * Indicates if the collection is spellchecked or not. + * + * @return boolean + */ + public function isSpellchecked() + { + return $this->isSpellchecked; + } + + /** + * @SuppressWarnings(PHPMD.CamelCaseMethodName) + * + * {@inheritdoc} + */ + protected function _renderFiltersBefore() + { + $searchRequest = $this->prepareRequest(); + + $this->queryResponse = $this->searchEngine->search($searchRequest); + + // Update the item count. + $this->_totalRecords = $this->queryResponse->count(); + + // Filter search results. The pagination has to be resetted since it is managed by the engine itself. + $docIds = array_map( + function (\Magento\Framework\Api\Search\Document $doc) { + return (int) $doc->getId(); + }, + $this->queryResponse->getIterator()->getArrayCopy() + ); + + if (empty($docIds)) { + $docIds[] = 0; + } + + $this->getSelect()->where('e.entity_id IN (?)', ['in' => $docIds]); + $this->_pageSize = false; + + $this->isSpellchecked = $searchRequest->isSpellchecked(); + + parent::_renderFiltersBefore(); + } + + /** + * @SuppressWarnings(PHPMD.CamelCaseMethodName) + * + * {@inheritDoc} + */ + protected function _afterLoad() + { + // Resort items according the search response. + $originalItems = $this->_items; + $this->_items = []; + + foreach ($this->queryResponse->getIterator() as $document) { + $documentId = $document->getId(); + if (isset($originalItems[$documentId])) { + $originalItems[$documentId]->setDocumentScore($document->getScore()); + $originalItems[$documentId]->setDocumentSource($document->getSource()); + $this->_items[$documentId] = $originalItems[$documentId]; + } + } + + return parent::_afterLoad(); + } + + /** + * Prepare the search request before it will be executed. + * + * @return RequestInterface + */ + private function prepareRequest() + { + // Store id and request name. + $storeId = $this->getStoreId(); + $searchRequestName = $this->searchRequestName; + + // Pagination params. + $size = $this->_pageSize ? $this->_pageSize : 20; + $from = $size * (max(1, $this->_curPage) - 1); + + // Query text. + $queryText = $this->queryText; + + // Setup sort orders. + $sortOrders = $this->prepareSortOrders(); + + $searchRequest = $this->requestBuilder->create( + $storeId, + $searchRequestName, + $from, + $size, + $queryText, + $sortOrders, + $this->filters, + $this->queryFilters, + $this->facets + ); + + return $searchRequest; + } + + /** + * Prepare sort orders for the request builder. + * + * @return array() + */ + private function prepareSortOrders() + { + $sortOrders = []; + + foreach ($this->_orders as $attribute => $direction) { + $sortParams = ['direction' => $direction]; + $sortField = $attribute; + $sortOrders[$sortField] = $sortParams; + } + + return $sortOrders; + } + + /** + * Load items count : + * - collection size + * + * @return void + */ + private function loadItemCounts() + { + $storeId = $this->getStoreId(); + $requestName = $this->searchRequestName; + + // Query text. + $queryText = $this->queryText; + + $searchRequest = $this->requestBuilder->create( + $storeId, + $requestName, + 0, + 0, + $queryText, + [], + $this->filters, + $this->queryFilters + ); + + $searchResponse = $this->searchEngine->search($searchRequest); + + $this->_totalRecords = $searchResponse->count(); + } +} diff --git a/src/module-elasticsuite-catalog/Model/ResourceModel/Eav/Indexer/Fulltext/Datasource/AbstractAttributeData.php b/src/module-elasticsuite-catalog/Model/ResourceModel/Eav/Indexer/Fulltext/Datasource/AbstractAttributeData.php index bb25ee5d0..f077a7cf3 100644 --- a/src/module-elasticsuite-catalog/Model/ResourceModel/Eav/Indexer/Fulltext/Datasource/AbstractAttributeData.php +++ b/src/module-elasticsuite-catalog/Model/ResourceModel/Eav/Indexer/Fulltext/Datasource/AbstractAttributeData.php @@ -13,6 +13,9 @@ namespace Smile\ElasticsuiteCatalog\Model\ResourceModel\Eav\Indexer\Fulltext\Datasource; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Store\Model\StoreManagerInterface; use Smile\ElasticsuiteCatalog\Model\ResourceModel\Eav\Indexer\Indexer; use Magento\Eav\Model\ResourceModel\Entity\Attribute\Collection as AttributeCollection; @@ -37,6 +40,29 @@ class AbstractAttributeData extends Indexer 'used_for_sort_by' => ['operator' => '=', 'value' => 1], ]; + /** + * @var null|string + */ + private $entityTypeId = null; + + /** + * AbstractAttributeData constructor. + * + * @param \Magento\Framework\App\ResourceConnection $resource Resource Connection + * @param \Magento\Store\Model\StoreManagerInterface $storeManager Store Manager + * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool Entity Metadata Pool + * @param string $entityType Entity Type + */ + public function __construct( + ResourceConnection $resource, + StoreManagerInterface $storeManager, + MetadataPool $metadataPool, + $entityType = null + ) { + $this->entityTypeId = $entityType; + parent::__construct($resource, $storeManager, $metadataPool); + } + /** * Allow to filter an attribute collection on attributes that are indexed into the search engine. * @@ -77,10 +103,16 @@ public function addIndexedFilterToAttributeCollection(AttributeCollection $attri */ public function getAttributesRawData($storeId, array $entityIds, $tableName, array $attributeIds) { - $select = $this->connection->select(); + $select = $this->connection->select(); + + // The field modelizing the link between entity table and attribute values table. Either row_id or entity_id. + $linkField = $this->getEntityMetaData($this->getEntityTypeId())->getLinkField(); + + // The legacy entity_id field. + $entityIdField = $this->getEntityMetaData($this->getEntityTypeId())->getIdentifierField(); $joinStoreValuesConditionClauses = [ - 't_default.entity_id = t_store.entity_id', + "t_default.$linkField = t_store.$linkField", 't_default.attribute_id = t_store.attribute_id', 't_store.store_id= ?', ]; @@ -90,13 +122,28 @@ public function getAttributesRawData($storeId, array $entityIds, $tableName, arr $storeId ); - $select->from(['t_default' => $tableName], ['entity_id', 'attribute_id']) + $select->from(['entity' => $this->getEntityMetaData($this->getEntityTypeId())->getEntityTable()], [$entityIdField]) + ->joinInner( + ['t_default' => $tableName], + new \Zend_Db_Expr("entity.{$linkField} = t_default.{$linkField}"), + ['attribute_id'] + ) ->joinLeft(['t_store' => $tableName], $joinStoreValuesCondition, []) ->where('t_default.store_id=?', 0) ->where('t_default.attribute_id IN (?)', $attributeIds) - ->where('t_default.entity_id IN (?)', $entityIds) + ->where("entity.{$entityIdField} IN (?)", $entityIds) ->columns(['value' => new \Zend_Db_Expr('COALESCE(t_store.value, t_default.value)')]); return $this->connection->fetchAll($select); } + + /** + * Get Entity Type Id. + * + * @return string + */ + protected function getEntityTypeId() + { + return $this->entityTypeId; + } } diff --git a/src/module-elasticsuite-catalog/Model/ResourceModel/Eav/Indexer/Indexer.php b/src/module-elasticsuite-catalog/Model/ResourceModel/Eav/Indexer/Indexer.php index 73242a943..c96c45c4f 100644 --- a/src/module-elasticsuite-catalog/Model/ResourceModel/Eav/Indexer/Indexer.php +++ b/src/module-elasticsuite-catalog/Model/ResourceModel/Eav/Indexer/Indexer.php @@ -15,6 +15,9 @@ namespace Smile\ElasticsuiteCatalog\Model\ResourceModel\Eav\Indexer; use Magento\Framework\App\ResourceConnection; +use Magento\Framework\EntityManager\EntityMetadataInterface; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Store\Model\StoreManagerInterface; use Smile\ElasticsuiteCore\Model\ResourceModel\Indexer\AbstractIndexer; /** @@ -32,6 +35,24 @@ class Indexer extends AbstractIndexer */ protected $storeManager; + /** + * @var \Magento\Framework\EntityManager\MetadataPool + */ + private $metadataPool; + + /** + * Indexer constructor. + * + * @param \Magento\Framework\App\ResourceConnection $resource Resource Connection + * @param \Magento\Store\Model\StoreManagerInterface $storeManager Store Manager + * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool Metadata Pool + */ + public function __construct(ResourceConnection $resource, StoreManagerInterface $storeManager, MetadataPool $metadataPool) + { + parent::__construct($resource, $storeManager); + $this->metadataPool = $metadataPool; + } + /** * Retrieve store root category id. * @@ -49,4 +70,16 @@ protected function getRootCategoryId($store) return $this->storeManager->getGroup($storeGroupId)->getRootCategoryId(); } + + /** + * Retrieve Metadata for an entity + * + * @param string $entityType The entity + * + * @return EntityMetadataInterface + */ + protected function getEntityMetaData($entityType) + { + return $this->metadataPool->getMetadata($entityType); + } } diff --git a/src/module-elasticsuite-catalog/Model/ResourceModel/Product/Fulltext/Collection.php b/src/module-elasticsuite-catalog/Model/ResourceModel/Product/Fulltext/Collection.php index c9399c0fc..3c586312c 100644 --- a/src/module-elasticsuite-catalog/Model/ResourceModel/Product/Fulltext/Collection.php +++ b/src/module-elasticsuite-catalog/Model/ResourceModel/Product/Fulltext/Collection.php @@ -185,7 +185,7 @@ public function getSize() /** * {@inheritDoc} */ - public function setOrder($attribute, $dir = \Magento\Framework\DB\Select::SQL_DESC) + public function setOrder($attribute, $dir = self::SORT_ORDER_DESC) { $this->_orders[$attribute] = $dir; @@ -203,6 +203,14 @@ public function addFieldToFilter($field, $condition = null) return $this; } + /** + * {@inheritDoc} + */ + public function addAttributeToSort($attribute, $dir = self::SORT_ORDER_ASC) + { + return $this->setOrder($attribute, $dir); + } + /** * Append a prebuilt (QueryInterface) query filter to the collection. * @@ -348,12 +356,20 @@ public function addIsInStockFilter() */ public function addSortFilterParameters($sortName, $sortField, $nestedPath = null, $nestedFilter = null) { - $this->_productLimitationFilters['sortParams'][$sortName] = [ + $sortParams = []; + + if (isset($this->_productLimitationFilters['sortParams'])) { + $sortParams = $this->_productLimitationFilters['sortParams']; + } + + $sortParams[$sortName] = [ 'sortField' => $sortField, 'nestedPath' => $nestedPath, 'nestedFilter' => $nestedFilter, ]; + $this->_productLimitationFilters['sortParams'] = $sortParams; + return $this; } @@ -482,10 +498,12 @@ private function prepareSortOrders() { $sortOrders = []; + $useProductuctLimitation = isset($this->_productLimitationFilters['sortParams']); + foreach ($this->_orders as $attribute => $direction) { $sortParams = ['direction' => $direction]; - if (isset($this->_productLimitationFilters['sortParams'][$attribute])) { + if ($useProductuctLimitation && isset($this->_productLimitationFilters['sortParams'][$attribute])) { $sortField = $this->_productLimitationFilters['sortParams'][$attribute]['sortField']; $sortParams = array_merge($sortParams, $this->_productLimitationFilters['sortParams'][$attribute]); } elseif ($attribute == 'price') { diff --git a/src/module-elasticsuite-catalog/Model/ResourceModel/Product/Indexer/Fulltext/Datasource/AttributeData.php b/src/module-elasticsuite-catalog/Model/ResourceModel/Product/Indexer/Fulltext/Datasource/AttributeData.php index cbb6c0dfb..116d87797 100644 --- a/src/module-elasticsuite-catalog/Model/ResourceModel/Product/Indexer/Fulltext/Datasource/AttributeData.php +++ b/src/module-elasticsuite-catalog/Model/ResourceModel/Product/Indexer/Fulltext/Datasource/AttributeData.php @@ -14,6 +14,8 @@ namespace Smile\ElasticsuiteCatalog\Model\ResourceModel\Product\Indexer\Fulltext\Datasource; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Framework\EntityManager\MetadataPool; use Smile\ElasticsuiteCatalog\Model\ResourceModel\Eav\Indexer\Fulltext\Datasource\AbstractAttributeData; use Magento\Framework\App\ResourceConnection; use Magento\Store\Model\StoreManagerInterface; @@ -50,14 +52,18 @@ class AttributeData extends AbstractAttributeData * * @param ResourceConnection $resource Database adpater. * @param StoreManagerInterface $storeManager Store manager. + * @param MetadataPool $metadataPool Metadata Pool. * @param ProductType $catalogProductType Product type. + * @param string $entityType Product entity type. */ public function __construct( ResourceConnection $resource, StoreManagerInterface $storeManager, - ProductType $catalogProductType + MetadataPool $metadataPool, + ProductType $catalogProductType, + $entityType = ProductInterface::class ) { - parent::__construct($resource, $storeManager); + parent::__construct($resource, $storeManager, $metadataPool, $entityType); $this->catalogProductType = $catalogProductType; } @@ -154,4 +160,14 @@ protected function getProductTypeInstance($typeId) return $this->productTypes[$typeId]; } + + /** + * Get Entity Id used by this indexer + * + * @return string + */ + protected function getEntityTypeId() + { + return ProductInterface::class; + } } diff --git a/src/module-elasticsuite-catalog/Model/ResourceModel/Product/Indexer/Fulltext/Datasource/CategoryData.php b/src/module-elasticsuite-catalog/Model/ResourceModel/Product/Indexer/Fulltext/Datasource/CategoryData.php index 3abd77738..4258b2037 100644 --- a/src/module-elasticsuite-catalog/Model/ResourceModel/Product/Indexer/Fulltext/Datasource/CategoryData.php +++ b/src/module-elasticsuite-catalog/Model/ResourceModel/Product/Indexer/Fulltext/Datasource/CategoryData.php @@ -14,8 +14,10 @@ namespace Smile\ElasticsuiteCatalog\Model\ResourceModel\Product\Indexer\Fulltext\Datasource; use Magento\Catalog\Api\Data\CategoryAttributeInterface; +use Magento\Catalog\Api\Data\CategoryInterface; use Magento\Eav\Model\Config; use Magento\Framework\App\ResourceConnection; +use Magento\Framework\EntityManager\MetadataPool; use Magento\Store\Model\StoreManagerInterface; use Smile\ElasticsuiteCatalog\Model\ResourceModel\Eav\Indexer\Indexer; @@ -51,14 +53,19 @@ class CategoryData extends Indexer /** * CategoryData constructor. * - * @param \Magento\Framework\App\ResourceConnection $resource Connection Resource - * @param \Magento\Store\Model\StoreManagerInterface $storeManager The store manager - * @param \Magento\Eav\Model\Config $eavConfig EAV Configuration + * @param \Magento\Framework\App\ResourceConnection $resource Connection Resource + * @param \Magento\Store\Model\StoreManagerInterface $storeManager The store manager + * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool Metadata Pool + * @param \Magento\Eav\Model\Config $eavConfig EAV Configuration */ - public function __construct(ResourceConnection $resource, StoreManagerInterface $storeManager, Config $eavConfig) - { + public function __construct( + ResourceConnection $resource, + StoreManagerInterface $storeManager, + MetadataPool $metadataPool, + Config $eavConfig + ) { $this->eavConfig = $eavConfig; - parent::__construct($resource, $storeManager); + parent::__construct($resource, $storeManager, $metadataPool); } /** @@ -162,9 +169,10 @@ private function loadCategoryNames($categoryIds, $storeId) if (!empty($loadCategoryIds)) { $select = $this->prepareCategoryNameSelect($loadCategoryIds, $storeId); + $entityIdField = $this->getEntityMetaData(CategoryInterface::class)->getIdentifierField(); foreach ($this->getConnection()->fetchAll($select) as $row) { - $categoryId = (int) $row['entity_id']; + $categoryId = (int) $row[$entityIdField]; $this->categoryNameCache[$storeId][$categoryId] = ''; if ((bool) $row['use_name']) { $this->categoryNameCache[$storeId][$categoryId] = $row['name']; @@ -188,20 +196,23 @@ private function prepareCategoryNameSelect($loadCategoryIds, $storeId) $rootCategoryId = (int) $this->storeManager->getStore($storeId)->getRootCategoryId(); $this->categoryNameCache[$storeId][$rootCategoryId] = ''; - $nameAttr = $this->getCategoryNameAttribute(); - $useNameAttr = $this->getUseNameInSearchAttribute(); + $nameAttr = $this->getCategoryNameAttribute(); + $useNameAttr = $this->getUseNameInSearchAttribute(); + $entityIdField = $this->getEntityMetaData(CategoryInterface::class)->getIdentifierField(); + $linkField = $this->getEntityMetaData(CategoryInterface::class)->getLinkField(); + $select = $this->connection->select(); - // Initialize retrieval of category name. - $select = $this->getConnection()->select() - ->from(['default_value' => $nameAttr->getBackendTable()], ['entity_id']) - ->where('default_value.entity_id != ?', $rootCategoryId) + $joinCondition = new \Zend_Db_Expr("cat.{$linkField} = default_value.{$linkField}"); + $select->from(['cat' => $this->getEntityMetaData(CategoryInterface::class)->getEntityTable()], [$entityIdField]) + ->joinInner(['default_value' => $nameAttr->getBackendTable()], $joinCondition, []) + ->where("cat.$entityIdField != ?", $rootCategoryId) ->where('default_value.store_id = ?', 0) ->where('default_value.attribute_id = ?', (int) $nameAttr->getAttributeId()) - ->where('default_value.entity_id IN (?)', $loadCategoryIds); + ->where("cat.$entityIdField IN (?)", $loadCategoryIds); // Join to check for use_name_in_product_search. $joinUseNameCond = sprintf( - "default_value.entity_id = use_name_default_value.entity_id" . + "default_value.$linkField = use_name_default_value.$linkField" . " AND use_name_default_value.attribute_id = %d AND use_name_default_value.store_id = %d", (int) $useNameAttr->getAttributeId(), 0 @@ -217,7 +228,7 @@ private function prepareCategoryNameSelect($loadCategoryIds, $storeId) // Multi store additional join to get scoped name value. $joinStoreNameCond = sprintf( - "default_value.entity_id = store_value.entity_id" . + "default_value.$linkField = store_value.$linkField" . " AND store_value.attribute_id = %d AND store_value.store_id = %d", (int) $nameAttr->getAttributeId(), (int) $storeId @@ -227,7 +238,7 @@ private function prepareCategoryNameSelect($loadCategoryIds, $storeId) // Multi store additional join to get scoped "use_name_in_product_search" value. $joinUseNameStoreCond = sprintf( - "default_value.entity_id = use_name_store_value.entity_id" . + "default_value.$linkField = use_name_store_value.$linkField" . " AND use_name_store_value.attribute_id = %d AND use_name_store_value.store_id = %d", (int) $useNameAttr->getAttributeId(), (int) $storeId diff --git a/src/module-elasticsuite-catalog/Model/ResourceModel/Product/Indexer/Fulltext/Datasource/InventoryData.php b/src/module-elasticsuite-catalog/Model/ResourceModel/Product/Indexer/Fulltext/Datasource/InventoryData.php index 75f20dbee..fc5db4f78 100644 --- a/src/module-elasticsuite-catalog/Model/ResourceModel/Product/Indexer/Fulltext/Datasource/InventoryData.php +++ b/src/module-elasticsuite-catalog/Model/ResourceModel/Product/Indexer/Fulltext/Datasource/InventoryData.php @@ -13,7 +13,9 @@ namespace Smile\ElasticsuiteCatalog\Model\ResourceModel\Product\Indexer\Fulltext\Datasource; use Magento\CatalogInventory\Api\StockRegistryInterface; +use \Magento\CatalogInventory\Api\StockConfigurationInterface; use Magento\Framework\App\ResourceConnection; +use Magento\Framework\EntityManager\MetadataPool; use Magento\Store\Model\StoreManagerInterface; use Smile\ElasticsuiteCatalog\Model\ResourceModel\Eav\Indexer\Indexer; @@ -31,6 +33,12 @@ class InventoryData extends Indexer */ private $stockRegistry; + + /** + * @var \Magento\CatalogInventory\Api\StockConfigurationInterface + */ + private $stockConfiguration; + /** * @var int[] */ @@ -39,17 +47,23 @@ class InventoryData extends Indexer /** * InventoryData constructor. * - * @param ResourceConnection $resource Database adapter. - * @param StoreManagerInterface $storeManager Store manager. - * @param StockRegistryInterface $stockRegistry Stock registry. + * @param ResourceConnection $resource Database adapter. + * @param StoreManagerInterface $storeManager Store manager. + * @param MetadataPool $metadataPool Metadata Pool + * @param StockRegistryInterface $stockRegistry Stock registry. + * @param StockConfigurationInterface $stockConfiguration Stock configuration. */ public function __construct( ResourceConnection $resource, StoreManagerInterface $storeManager, - StockRegistryInterface $stockRegistry + MetadataPool $metadataPool, + StockRegistryInterface $stockRegistry, + StockConfigurationInterface $stockConfiguration ) { - $this->stockRegistry = $stockRegistry; - parent::__construct($resource, $storeManager); + $this->stockRegistry = $stockRegistry; + $this->stockConfiguration = $stockConfiguration; + + parent::__construct($resource, $storeManager, $metadataPool); } /** @@ -68,7 +82,7 @@ public function loadInventoryData($storeId, $productIds) $select = $this->getConnection()->select() ->from(['ciss' => $this->getTable('cataloginventory_stock_status')], ['product_id', 'stock_status', 'qty']) ->where('ciss.stock_id = ?', $stockId) - ->where('ciss.website_id = ?', $websiteId) + ->where('ciss.website_id = ?', $this->stockConfiguration->getDefaultScopeId()) ->where('ciss.product_id IN(?)', $productIds); return $this->getConnection()->fetchAll($select); diff --git a/src/module-elasticsuite-catalog/Observer/RedirectIfOneResult.php b/src/module-elasticsuite-catalog/Observer/RedirectIfOneResult.php new file mode 100644 index 000000000..0e7380336 --- /dev/null +++ b/src/module-elasticsuite-catalog/Observer/RedirectIfOneResult.php @@ -0,0 +1,117 @@ + + * @copyright 2016 Smile + * @license Open Software License ("OSL") v. 3.0 + */ +namespace Smile\ElasticsuiteCatalog\Observer; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Model\Layer\Resolver; +use Magento\CatalogSearch\Helper\Data; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Event\Observer; +use Magento\Framework\Event\ObserverInterface; +use \Magento\Framework\Message\ManagerInterface; + +/** + * Observer that redirect to the product page if this is the only search result. + * + * @category Smile + * @package Smile\ElasticsuiteCatalog + * @author Romain Ruaud + */ +class RedirectIfOneResult implements ObserverInterface +{ + /** + * Constant for configuration field location. + */ + const REDIRECT_SETTINGS_CONFIG_XML_FLAG = 'smile_elasticsuite_catalogsearch_settings/catalogsearch/redirect_if_one_result'; + + /** + * Catalog Layer Resolver + * + * @var Resolver + */ + private $layerResolver; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var ManagerInterface + */ + private $messageManager; + + /** + * @var \Magento\CatalogSearch\Helper\Data + */ + private $helper; + + /** + * RedirectIfOneResult constructor. + * + * @param \Magento\Catalog\Model\Layer\Resolver $layerResolver Layer Resolver + * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig Scope Configuration + * @param \Magento\CatalogSearch\Helper\Data $catalogSearchHelper Catalog Search Helper + * @param \Magento\Framework\Message\ManagerInterface $messageManager Message Manager + */ + public function __construct( + Resolver $layerResolver, + ScopeConfigInterface $scopeConfig, + Data $catalogSearchHelper, + ManagerInterface $messageManager + ) { + $this->layerResolver = $layerResolver; + $this->scopeConfig = $scopeConfig; + $this->messageManager = $messageManager; + $this->helper = $catalogSearchHelper; + } + + /** + * Process redirect to the product page if this is the only search result. + * + * @param Observer $observer The observer + * @event controller_action_postdispatch_catalogsearch_result_index + * + * @return void + */ + public function execute(\Magento\Framework\Event\Observer $observer) + { + if ($this->scopeConfig->isSetFlag(self::REDIRECT_SETTINGS_CONFIG_XML_FLAG)) { + $layer = $this->layerResolver->get(); + $layerState = $layer->getState(); + + if (count($layerState->getFilters()) === 0) { + $productCollection = $layer->getProductCollection(); + if ($productCollection->getCurPage() === 1 && $productCollection->getSize() === 1) { + /** @var \Magento\Catalog\Api\Data\ProductInterface $product */ + $product = $productCollection->getFirstItem(); + if ($product->getId()) { + $this->addRedirectMessage($product); + $observer->getControllerAction()->getResponse()->setRedirect($product->getProductUrl()); + } + } + } + } + } + + /** + * Append message to the customer session to inform he has been redirected + * + * @param \Magento\Catalog\Api\Data\ProductInterface $product The product being redirected to. + */ + private function addRedirectMessage(ProductInterface $product) + { + $message = __("%1 is the only product matching your '%2' research.", $product->getName(), $this->helper->getEscapedQueryText()); + $this->messageManager->addSuccessMessage($message); + } +} diff --git a/src/module-elasticsuite-catalog/Setup/InstallData.php b/src/module-elasticsuite-catalog/Setup/InstallData.php index 2830bb180..84d9743b1 100644 --- a/src/module-elasticsuite-catalog/Setup/InstallData.php +++ b/src/module-elasticsuite-catalog/Setup/InstallData.php @@ -13,9 +13,11 @@ */ namespace Smile\ElasticsuiteCatalog\Setup; +use Magento\Catalog\Api\Data\CategoryInterface; use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Catalog\Model\Category; use Magento\Eav\Setup\EavSetupFactory; +use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Setup\InstallDataInterface; use Magento\Framework\Setup\ModuleContextInterface; use Magento\Framework\Setup\ModuleDataSetupInterface; @@ -24,6 +26,8 @@ /** * Catalog installer * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * * @category Smile * @package Smile\ElasticsuiteCatalog * @author Romain Ruaud @@ -43,13 +47,20 @@ class InstallData implements InstallDataInterface */ private $eavSetup; + /** + * @var MetadataPool + */ + private $metadataPool; + /** * Class Constructor * * @param EavSetupFactory $eavSetupFactory Eav setup factory. + * @param MetadataPool $metadataPool Metadata Pool. */ - public function __construct(EavSetupFactory $eavSetupFactory) + public function __construct(EavSetupFactory $eavSetupFactory, MetadataPool $metadataPool) { + $this->metadataPool = $metadataPool; $this->eavSetupFactory = $eavSetupFactory; } @@ -115,7 +126,7 @@ private function updateCategoryIsAnchorAttribute() { $this->eavSetup->updateAttribute(Category::ENTITY, 'is_anchor', 'frontend_input', 'hidden'); $this->eavSetup->updateAttribute(Category::ENTITY, 'is_anchor', 'source_model', null); - $this->updateAttributeDefaultValue(Category::ENTITY, 'is_anchor', 1); + $this->updateAttributeDefaultValue(Category::ENTITY, 'is_anchor', 1, [\Magento\Catalog\Model\Category::TREE_ROOT_ID]); } /** @@ -125,28 +136,37 @@ private function updateCategoryIsAnchorAttribute() * @param integer|string $entityTypeId Target entity id. * @param integer|string $attributeId Target attribute id. * @param mixed $value Value to be set. + * @param array $excludedIds List of categories that should not be updated during the process. * * @return void */ - private function updateAttributeDefaultValue($entityTypeId, $attributeId, $value) + private function updateAttributeDefaultValue($entityTypeId, $attributeId, $value, $excludedIds = []) { - $entityTable = $this->eavSetup->getEntityType($entityTypeId, 'entity_table'); + $setup = $this->eavSetup->getSetup(); + $entityTable = $setup->getTable($this->eavSetup->getEntityType($entityTypeId, 'entity_table')); $attributeTable = $this->eavSetup->getAttributeTable($entityTypeId, $attributeId); if (!is_int($attributeId)) { $attributeId = $this->eavSetup->getAttributeId($entityTypeId, $attributeId); } + // Retrieve the primary key name. May differs if the staging module is activated or not. + $linkField = $this->metadataPool->getMetadata(CategoryInterface::class)->getLinkField(); + $entitySelect = $this->getConnection()->select(); $entitySelect->from( $entityTable, - [new \Zend_Db_Expr("{$attributeId} as attribute_id"), 'entity_id', new \Zend_Db_Expr("{$value} as value")] + [new \Zend_Db_Expr("{$attributeId} as attribute_id"), $linkField, new \Zend_Db_Expr("{$value} as value")] ); + if (!empty($excludedIds)) { + $entitySelect->where("entity_id NOT IN(?)", $excludedIds); + } + $insertQuery = $this->getConnection()->insertFromSelect( $entitySelect, $attributeTable, - ['attribute_id', 'entity_id', 'value'], + ['attribute_id', $linkField, 'value'], AdapterInterface::INSERT_ON_DUPLICATE ); diff --git a/src/module-elasticsuite-catalog/Setup/UpgradeData.php b/src/module-elasticsuite-catalog/Setup/UpgradeData.php new file mode 100644 index 000000000..f74d1e77b --- /dev/null +++ b/src/module-elasticsuite-catalog/Setup/UpgradeData.php @@ -0,0 +1,111 @@ + + * @copyright 2016 Smile + * @license Open Software License ("OSL") v. 3.0 + */ +namespace Smile\ElasticsuiteCatalog\Setup; + +use Magento\Eav\Setup\EavSetup; +use Magento\Framework\Setup\UpgradeDataInterface; +use Magento\Catalog\Model\Category; +use Magento\Eav\Setup\EavSetupFactory; +use Magento\Framework\Setup\ModuleContextInterface; +use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Framework\DB\Adapter\AdapterInterface; + +/** + * Catalog Data Upgrade + * + * @category Smile + * @package Smile\ElasticsuiteCatalog + * @author Romain Ruaud + */ +class UpgradeData implements UpgradeDataInterface +{ + /** + * EAV setup factory + * + * @var EavSetupFactory + */ + private $eavSetupFactory; + + /** + * @var EavSetup + */ + private $eavSetup; + + /** + * Class Constructor + * + * @param EavSetupFactory $eavSetupFactory Eav setup factory. + */ + public function __construct(EavSetupFactory $eavSetupFactory) + { + $this->eavSetupFactory = $eavSetupFactory; + } + + /** + * Upgrade the module data. + * + * @param ModuleDataSetupInterface $setup The setup interface + * @param ModuleContextInterface $context The module Context + * + * @return void + */ + public function upgrade(ModuleDataSetupInterface $setup, ModuleContextInterface $context) + { + $setup->startSetup(); + $this->eavSetup = $this->eavSetupFactory->create(['setup' => $setup]); + + if (version_compare($context->getVersion(), '1.2.0', '<')) { + $this->updateCategorySearchableAttributes(); + } + + $setup->endSetup(); + } + + /** + * Update some categories attributes to have them indexed into ES. + * Basically : + * - Name (indexable and searchable + * - Description (indexable and searchable) + * - Url Path (indexable) + */ + private function updateCategorySearchableAttributes() + { + $setup = $this->eavSetup->getSetup(); + $connection = $setup->getConnection(); + $table = $setup->getTable('catalog_eav_attribute'); + + // Set Name and description indexable and searchable. + $attributeIds = [ + $this->eavSetup->getAttributeId(\Magento\Catalog\Model\Category::ENTITY, 'name'), + $this->eavSetup->getAttributeId(\Magento\Catalog\Model\Category::ENTITY, 'description'), + ]; + + foreach (['is_searchable', 'is_used_in_spellcheck'] as $configField) { + foreach ($attributeIds as $attributeId) { + $connection->update( + $table, + [$configField => 1], + $connection->quoteInto('attribute_id = ?', $attributeId) + ); + } + } + + // Set url_path indexable. + $urlPathAttributeId = $this->eavSetup->getAttributeId(\Magento\Catalog\Model\Category::ENTITY, 'url_path'); + $connection->update( + $table, + ['is_searchable' => 1], + $connection->quoteInto('attribute_id = ?', $urlPathAttributeId) + ); + } +} diff --git a/src/module-elasticsuite-catalog/composer.json b/src/module-elasticsuite-catalog/composer.json index 29d24e7aa..ac7243731 100644 --- a/src/module-elasticsuite-catalog/composer.json +++ b/src/module-elasticsuite-catalog/composer.json @@ -20,11 +20,11 @@ ], "require": { "php": "~5.5.0|~5.6.0|~7.0.0", - "magento/framework": "*", - "magento/module-store": "*", - "magento/module-backend": "*", - "magento/module-catalog": "*", - "magento/module-catalog-search": "*", + "magento/framework": ">=100.1.0", + "magento/module-store": ">=100.1.0", + "magento/module-backend": ">=100.1.0", + "magento/module-catalog": ">=100.1.0", + "magento/module-catalog-search": ">=100.1.0", "magento/magento-composer-installer": "*", "elasticsearch/elasticsearch": "2.1.*" }, diff --git a/src/module-elasticsuite-catalog/etc/adminhtml/system.xml b/src/module-elasticsuite-catalog/etc/adminhtml/system.xml index 4605fe070..8eff96870 100644 --- a/src/module-elasticsuite-catalog/etc/adminhtml/system.xml +++ b/src/module-elasticsuite-catalog/etc/adminhtml/system.xml @@ -25,6 +25,29 @@ + + + + + integer + + + + + +
+ + smile_elasticsuite + Magento_Backend::smile_elasticsuite_autocomplete + + + + + + Magento\Config\Model\Config\Source\Yesno + + +
diff --git a/src/module-elasticsuite-catalog/etc/config.xml b/src/module-elasticsuite-catalog/etc/config.xml index 7146548bb..a3a599f1d 100644 --- a/src/module-elasticsuite-catalog/etc/config.xml +++ b/src/module-elasticsuite-catalog/etc/config.xml @@ -25,6 +25,14 @@ 5 + + 3 + + + + 1 + + diff --git a/src/module-elasticsuite-catalog/etc/di.xml b/src/module-elasticsuite-catalog/etc/di.xml index 01e2c7da2..1ce60220a 100644 --- a/src/module-elasticsuite-catalog/etc/di.xml +++ b/src/module-elasticsuite-catalog/etc/di.xml @@ -140,9 +140,17 @@ + + + Magento\Catalog\Api\Data\CategoryInterface + + + Smile\ElasticsuiteCatalog\Helper\CategoryAttribute + Smile\ElasticsuiteCatalog\Model\ResourceModel\Category\Indexer\Fulltext\Datasource\AttributeData diff --git a/src/module-elasticsuite-catalog/etc/elasticsuite_search_request.xml b/src/module-elasticsuite-catalog/etc/elasticsuite_search_request.xml index dcbcbb66f..59b32cec3 100644 --- a/src/module-elasticsuite-catalog/etc/elasticsuite_search_request.xml +++ b/src/module-elasticsuite-catalog/etc/elasticsuite_search_request.xml @@ -25,4 +25,5 @@ + diff --git a/src/module-elasticsuite-catalog/etc/events.xml b/src/module-elasticsuite-catalog/etc/events.xml new file mode 100644 index 000000000..a8034c070 --- /dev/null +++ b/src/module-elasticsuite-catalog/etc/events.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/src/module-elasticsuite-catalog/etc/frontend/di.xml b/src/module-elasticsuite-catalog/etc/frontend/di.xml index b98c380ec..a9a3d7714 100644 --- a/src/module-elasticsuite-catalog/etc/frontend/di.xml +++ b/src/module-elasticsuite-catalog/etc/frontend/di.xml @@ -63,6 +63,7 @@ Smile\ElasticsuiteCatalog\Model\Autocomplete\Product\DataProvider + Smile\ElasticsuiteCatalog\Model\Autocomplete\Category\DataProvider @@ -104,4 +105,19 @@ Smile\ElasticsuiteCatalog\Model\Layer\Filter\Item\CategoryFactory + + + + + categoryFilterList + + + + + + searchFilterList + + + + diff --git a/src/module-elasticsuite-catalog/etc/module.xml b/src/module-elasticsuite-catalog/etc/module.xml index 3dfa472a7..fe7b64041 100644 --- a/src/module-elasticsuite-catalog/etc/module.xml +++ b/src/module-elasticsuite-catalog/etc/module.xml @@ -17,7 +17,7 @@ */ --> - + diff --git a/src/module-elasticsuite-catalog/i18n/en_US.csv b/src/module-elasticsuite-catalog/i18n/en_US.csv index 1bbe8646b..a5d0a9f47 100755 --- a/src/module-elasticsuite-catalog/i18n/en_US.csv +++ b/src/module-elasticsuite-catalog/i18n/en_US.csv @@ -41,4 +41,12 @@ OK,OK "Is Spellchecked","Is Spellchecked" Yes,Yes No,No -"Maximum number of products to display in autocomplete results.","Nombre maximum de produits à afficher dans les résultats de l'autocomplétion." +"Maximum number of products to display in autocomplete results.","Maximum number of products to display in autocomplete results." +"Category Autocomplete","Category Autocomplete" +"Maximum number of categories to display in autocomplete results.","Maximum number of categories to display in autocomplete results." +"Use Category Name in product search","Use Category Name in product search" +"Catalog Search","Catalog Search" +"Catalog Search Configuration","Catalog Search Configuration" +"Redirect to product page if only one result","Redirect to product page if only one result" +"If there is only one product matching a given search query, the user will be redirect to this product page.","If there is only one product matching a given search query, the user will be redirect to this product page." +"%1 is the only product matching your '%2' research.","%1 is the only product matching your '%2' research." diff --git a/src/module-elasticsuite-catalog/i18n/fr_FR.csv b/src/module-elasticsuite-catalog/i18n/fr_FR.csv index f4d85988c..33bbbb74f 100755 --- a/src/module-elasticsuite-catalog/i18n/fr_FR.csv +++ b/src/module-elasticsuite-catalog/i18n/fr_FR.csv @@ -42,3 +42,11 @@ OK,OK Yes,Oui No,Non "Maximum number of products to display in autocomplete results.","Nombre maximum de produits à afficher dans les résultats de l'autocomplétion." +"Category Autocomplete","Autocomplétion des catégories" +"Maximum number of categories to display in autocomplete results.","Nombre maximum de catégories à afficher dans les résultats de l'autocomplétion." +"Use Category Name in product search","Utiliser le nom de la catégorie dans les résultats de recherche" +"Catalog Search","Recherche Catalogue" +"Catalog Search Configuration","Configuration de la recherche catalogue" +"Redirect to product page if only one result","Rediriger vers la fiche produit pour un résultat unique" +"If there is only one product matching a given search query, the user will be redirect to this product page.","Si une recherche ne renvoie qu'un seul produit, l'utilisateur sera redirigé vers la fiche de ce produit." +"%1 is the only product matching your '%2' research.","%1 est le seul produit correspondant à votre recherche : '%2'" diff --git a/src/module-elasticsuite-catalog/view/adminhtml/requirejs-config.js b/src/module-elasticsuite-catalog/view/adminhtml/requirejs-config.js deleted file mode 100644 index 1e6ece968..000000000 --- a/src/module-elasticsuite-catalog/view/adminhtml/requirejs-config.js +++ /dev/null @@ -1,21 +0,0 @@ -/** - * DISCLAIMER - * - * Do not edit or add to this file if you wish to upgrade Smile Elastic Suite to newer - * versions in the future. - * - * - * @category Smile - * @package Smile\ElasticsuiteCatalog - * @author Aurelien FOUCRET - * @copyright 2016 Smile - * @license Open Software License ("OSL") v. 3.0 - */ - -var config = { - map: { - '*': { - adminProductSorter: 'Smile_ElasticsuiteCatalog/js/catalog/product/form/renderer/sort' - } - } -}; diff --git a/src/module-elasticsuite-catalog/view/adminhtml/templates/catalog/product/form/renderer/sort.phtml b/src/module-elasticsuite-catalog/view/adminhtml/templates/catalog/product/form/renderer/sort.phtml deleted file mode 100644 index d9ea2a8d9..000000000 --- a/src/module-elasticsuite-catalog/view/adminhtml/templates/catalog/product/form/renderer/sort.phtml +++ /dev/null @@ -1,31 +0,0 @@ - - * @copyright 2016 Smile - * @license Open Software License ("OSL") v. 3.0 - */ -?> - - - -
-
- -
-
- - diff --git a/src/module-elasticsuite-catalog/view/adminhtml/ui_component/category_form.xml b/src/module-elasticsuite-catalog/view/adminhtml/ui_component/category_form.xml new file mode 100644 index 000000000..d14c02dba --- /dev/null +++ b/src/module-elasticsuite-catalog/view/adminhtml/ui_component/category_form.xml @@ -0,0 +1,57 @@ + + + + +
+ +
+ + + + hidden + 1 + + + +
+ +
+ + + + 100 + boolean + checkbox + category + toggle + Use Category Name in product search + + 1 + 0 + + + false + + 1 + + + +
+
diff --git a/src/module-elasticsuite-catalog/view/adminhtml/web/css/source/_module.less b/src/module-elasticsuite-catalog/view/adminhtml/web/css/source/_module.less index 8ad695027..90266916c 100644 --- a/src/module-elasticsuite-catalog/view/adminhtml/web/css/source/_module.less +++ b/src/module-elasticsuite-catalog/view/adminhtml/web/css/source/_module.less @@ -1,6 +1,53 @@ +// /** +// * DISCLAIMER +// * +// * Do not edit or add to this file if you wish to upgrade Smile Elastic Suite to newer +// * versions in the future. +// * +// * +// * @category Smile +// * @package Smile\ElasticsuiteCatalog +// * @author Aurelien FOUCRET +// * @copyright 2016 Smile +// * @license Open Software License ("OSL") v. 3.0 +// */ + + .elasticsuite-admin-product-sorter { + margin: 40px 0; + + span.title { + font-size: 1.7rem; + font-weight: 600; + letter-spacing: .025em; + } + + .bottom-links { + width: 40%; + margin: 10px auto; + border: 1px solid #cccccc; + text-align: center; + + a.show-more-link { + padding: 15px 10px; + width: 100%; + display: block; + } + } + + .admin__data-grid-loading-mask { + position: fixed; + } + + .elasticsuite-admin-product-sorter-empty { + margin: 20px 100px 20px 100px; + } + .product-list { + + margin: 20px 40px 0; + li.product-list-item, li.product-list-item-placeholder { display: inline-block; @@ -13,7 +60,7 @@ h1 { font-size: 1.2em; font-weight: 600; - line-height: 2.05em; + line-height: 1.9em; margin: 0 0 2px; span { @@ -28,7 +75,7 @@ .info { position: relative; background: rgba(44,44,44, 0.75); - width: 192px; + width: 100%; margin-top: -31px; color: #FFF; padding: 4px; @@ -45,13 +92,10 @@ } .draggable-handle { - display: block; - float: left; - margin: 10px; - cursor: move; + &:extend(.abs-draggable-handle all); + padding: 0; float: left; - margin: 10px 9px 0px 5px; - transform: scale(1.2); + margin: 3px 0 0 0; } @@ -67,9 +111,16 @@ } } - li.product-list-item.manual-sorting { - .draggable-handle { - background: none; + .admin__actions-switch { + margin: 10px; + font-weight: bold; + color: #666666; + } + + li.product-list-item.automatic-sorting { + .draggable-handle::before { + opacity: 0; + cursor: default; } } } diff --git a/src/module-elasticsuite-catalog/view/adminhtml/web/js/catalog/product/form/renderer/sort.js b/src/module-elasticsuite-catalog/view/adminhtml/web/js/catalog/product/form/renderer/sort.js deleted file mode 100644 index 9eecf7e8e..000000000 --- a/src/module-elasticsuite-catalog/view/adminhtml/web/js/catalog/product/form/renderer/sort.js +++ /dev/null @@ -1,261 +0,0 @@ -/** - * DISCLAIMER - * - * Do not edit or add to this file if you wish to upgrade Smile Elastic Suite to newer - * versions in the future. - * - * - * @category Smile - * @package Smile\ElasticsuiteCatalog - * @author Aurelien FOUCRET - * @copyright 2016 Smile - * @license Open Software License ("OSL") v. 3.0 - */ - -/*jshint browser:true jquery:true*/ -/*global console*/ - -define([ - 'uiComponent', - 'jquery', - 'Smile.ES.FormListener', - 'Magento_Catalog/js/price-utils', - 'mage/translate' -], function (Component, $, FormListener, priceUtil) { - - 'use strict'; - - var Product = Component.extend({ - initialize : function () { - this._super(); - this.observe(['position']); - this.setPosition(this.data.position); - }, - - setPosition : function (position) { - if (position) { - position = parseInt(position, 10); - } - - this.position(position); - }, - - compareTo : function(product) { - var result = 0; - result = this.hasPosition() && product.hasPosition() ? this.getPosition() - product.getPosition() : 0; - result = result === 0 && this.hasPosition() ? -1 : result; - result = result === 0 && product.hasPosition() ? 1 : result; - result = result === 0 ? product.getScore() - this.getScore() : result; - result = result === 0 ? product.getId() - this.getId(): result; - - return result; - }, - - getPosition : function () { return this.position(); }, - - hasPosition : function () { return this.getPosition() !== undefined && this.getPosition() !== null; }, - - getFormattedPrice : function () { return priceUtil.formatPrice(this.data.price, this.data.priceFormat); }, - - getId : function () { return parseInt(this.data.id, 10); }, - - getScore : function () { return parseFloat(this.data.score); }, - - getImageUrl : function () { return this.data.image; }, - - getName : function () { return this.data.name; }, - - getIsInStock : function () { return Boolean(this.data['is_in_stock']) }, - - getStockLabel : function () { return this.getIsInStock() === true ? $.mage.__('In Stock') : $.mage.__('Out Of Stock'); } - }); - - var productSorterComponent = Component.extend({ - - initialize : function () { - this._super(); - - this.products = []; - this.countTotalProducts = 0; - this.currentSize = this.pageSize; - - this.addListners(); - this.observe(['products', 'countTotalProducts', 'currentSize']); - this.loadProducts(); - }, - - addListners : function () { - // Reload the product list when something change into the form. - var formListenerChangeEvent = 'formListener:' + this.targetElementName; - this.formListener = new FormListener(this.formId, formListenerChangeEvent, this.refreshElements); - $(document).bind(formListenerChangeEvent, this.loadProducts.bind(this)); - }, - - loadProducts : function () { - if (this.loadXhr) { - this.loadXhr.abort(); - } - this.loadXhr = $.post(this.loadUrl, this.getLoadParams(), this.onProductLoad.bind(this)); - }, - - getLoadParams : function() { - var formData = this.formListener.serializeArray(); - - if (Array.isArray(this.savedPositions)) { - this.savedPositions = {}; - } - - var positionedProducts = this.isLoaded ? this.getEditPositions() : this.savedPositions; - - Object.keys(positionedProducts).each(function(productId) { - formData.push({name: 'product_position[' + productId + ']', value: positionedProducts[productId]}); - }); - - formData.push({name: 'page_size', value: this.currentSize()}); - - return formData; - }, - - onProductLoad : function (loadedData) { - this.isLoaded = true; - this.products(loadedData.products.map(this.createProduct.bind(this))); - this.countTotalProducts(parseInt(loadedData.size, 10)); - this.currentSize(Math.max(this.currentSize(), this.products().length)); - this.formListener.startListener(); - - }, - - createProduct : function(productData) { - productData.priceFormat = this.priceFormat; - - if (this.products() !== undefined && this.getEditPositions()[productData.id]) { - productData.position = this.getEditPositions()[productData.id]; - } else if (this.savedPositions[productData.id]) { - productData.position = this.savedPositions[productData.id]; - } - - return new Product({data : productData}); - }, - - getSortedProducts : function () { - var products = this.products(); - products.sort(function (product1, product2) { return product1.compareTo(product2); }); - return products; - }, - - getSerializedSortOrder: function () { - return JSON.stringify(this.getEditPositions()); - }, - - getEditPositions : function() { - var serializedProductPosition = {}; - - this.products() - .filter(function (product) { - return product.hasPosition(); - }) - .each(function (product) { - serializedProductPosition[product.getId()] = product.getPosition(); - }); - return serializedProductPosition; - }, - - hasProducts: function() { - return this.products().length > 0; - }, - - hasMoreProducts: function() { - return this.products().length < this.countTotalProducts(); - }, - - showMoreProducts: function() - { - this.currentSize(this.currentSize() + this.pageSize); - this.loadProducts(); - }, - - getProductById : function (productId) { - var product = null; - productId = parseInt(productId, 10); - this.products().each(function(currentProduct) { - if (currentProduct.getId() === productId) { - product = currentProduct; - } - }); - return product; - }, - - enableSortableList: function (element, component) { - $(element).sortable({ - items : "li:not('.manual-sorting')", - helper : 'clone', - handle : '.draggable-handle', - placeholder : 'product-list-item-placeholder', - update : component.onSortUpdate.bind(component) - }); - $(element).disableSelection(); - }, - - onSortUpdate : function(event, ui) - { - var productId = ui.item.attr('data-product-id'); - var position = 1; - - var previousProductId = ui.item.prev('li.product-list-item').attr('data-product-id'); - if (previousProductId !== undefined) { - var previousProduct = this.getProductById(previousProductId); - position = parseInt(previousProduct.getPosition(), 10) + 1; - } - - this.getProductById(productId).setPosition(position); - - ui.item.nextAll('li.product-list-item').each(function (index, element) { - var currentProduct = this.getProductById(element.getAttribute('data-product-id')); - if(currentProduct.getPosition()) { - position = position + 1; - currentProduct.setPosition(position); - } - }.bind(this)) - }, - - toggleSortType: function(product) { - if (product.getPosition() !== undefined) { - var lastProduct = this.getSortedProducts()[this.products().length -1]; - if (lastProduct.hasPosition() || lastProduct.getScore() >= product.getScore()) { - this.loadProducts(); - } - product.setPosition(undefined); - if (this.savedPositions[product.getId()]) { - delete this.savedPositions[product.getId()] - } - } else { - var allPositions = this.products() - .filter(function (product) { return product.hasPosition(); }) - .map(function (product) { return product.getPosition(); }) - .concat([0]); - - var maxPosition = Math.max.apply(null, allPositions); - - product.setPosition(maxPosition + 1); - } - }, - - getAutomaticSortLabel : function () { - return $.mage.__('Automatic Sort'); - }, - - getManualSortLabel : function () { - return $.mage.__('Manual Sort'); - }, - - getShowMoreLabel : function () { - return $.mage.__('Show more'); - }, - - getEmptyListMessage : function() { - return $.mage.__('Your product selection is empty.'); - } - }); - - return productSorterComponent; -}); \ No newline at end of file diff --git a/src/module-elasticsuite-catalog/view/adminhtml/web/js/form/element/product-sorter.js b/src/module-elasticsuite-catalog/view/adminhtml/web/js/form/element/product-sorter.js new file mode 100644 index 000000000..0b185dc08 --- /dev/null +++ b/src/module-elasticsuite-catalog/view/adminhtml/web/js/form/element/product-sorter.js @@ -0,0 +1,210 @@ +/** + * DISCLAIMER + * + * Do not edit or add to this file if you wish to upgrade Smile Elastic Suite to newer + * versions in the future. + * + * + * @category Smile + * @package Smile\ElasticsuiteCatalog + * @author Aurelien FOUCRET + * @copyright 2016 Smile + * @license Open Software License ("OSL") v. 3.0 + */ + +define([ + 'Magento_Ui/js/form/element/abstract', + 'jquery', + 'Smile_ElasticsuiteCatalog/js/form/element/product-sorter/item', + 'MutationObserver' +], function (Component, $, Product) { + 'use strict'; + + return Component.extend({ + defaults: { + showSpinner: true, + template: "Smile_ElasticsuiteCatalog/form/element/product-sorter", + refreshFields: {}, + maxRefreshInterval: 1000, + imports: { + formData: "${ $.provider }:data" + }, + messages : { + blockTitle : $.mage.__('Product Preview and Sorting'), + emptyText : $.mage.__('Your product selection is empty.'), + automaticSort : $.mage.__('Automatic Sort'), + manualSort : $.mage.__('Manual Sort'), + showMore : $.mage.__('Show more') + } + }, + + initialize: function () + { + this.updateImports(arguments[0]); + this._super(); + + this.editPositions = JSON.parse(this.value()); + this.products = []; + this.countTotalProducts = 0; + this.pageSize = parseInt(this.pageSize, 10); + this.currentSize = this.pageSize; + + this.observe(['products', 'countTotalProducts', 'currentSize', 'editPositions', 'loading', 'showSpinner']); + + this.editPositions.subscribe(function () { this.value(JSON.stringify(this.editPositions())); }.bind(this)); + }, + + updateImports: function (config) { + Object.keys(config.refreshFields).each (function (fieldName) { + fieldName = '${ $.provider }:data.' + fieldName; + + if (config.listens === undefined) { + config.listens = {} + } + config.listens[fieldName] = "refreshProductList"; + }); + }, + + refreshProductList: function () { + if (this.refreshRateLimiter !== undefined) { + clearTimeout(); + } + + this.loading(true); + + this.refreshRateLimiter = setTimeout(function () { + var formData = this.formData; + Object.keys(this.editPositions()).each(function (productId) { + formData['product_position[' + productId + ']'] = this.editPositions()[productId]; + }.bind(this)); + + formData['page_size'] = this.currentSize(); + this.loadXhr = $.post(this.loadUrl, this.formData, this.onProductListLoad.bind(this)); + }.bind(this), this.maxRefreshInterval); + }, + + onProductListLoad: function (loadedData) { + var products = this.sortProduct(loadedData.products.map(this.createProduct.bind(this))); + this.products(products); + this.countTotalProducts(parseInt(loadedData.size, 10)); + this.currentSize(Math.max(this.currentSize(), this.products().length)); + + var productIds = products.map(function (product) { return product.getId() }); + var editPositions = this.editPositions(); + + for (var productId in editPositions) { + if ($.inArray(parseInt(productId, 10), productIds) < 0) { + delete editPositions[productId]; + } + } + + this.editPositions(editPositions); + this.loading(false); + }, + + createProduct: function (productData) { + productData.priceFormat = this.priceFormat; + if (this.editPositions()[productData.id]) { + productData.position = this.editPositions()[productData.id]; + } + return new Product({data : productData}); + }, + + hasProducts: function () { + return this.products().length > 0; + }, + + hasMoreProducts: function () { + return this.products().length < this.countTotalProducts(); + }, + + showMoreProducts: function () { + this.currentSize(this.currentSize() + this.pageSize); + this.refreshProductList(); + }, + + sortProduct : function (products) { + products.sort(function (product1, product2) { return product1.compareTo(product2); }); + return products; + }, + + getProductById : function (productId) { + var product = null; + productId = parseInt(productId, 10); + this.products().each(function (currentProduct) { + if (currentProduct.getId() === productId) { + product = currentProduct; + } + }); + return product; + }, + + enableSortableList: function (element, component) { + $(element).sortable({ + items : "li:not(.automatic-sorting)", + helper : 'clone', + handle : '.draggable-handle', + placeholder : 'product-list-item-placeholder', + update : component.onSortUpdate.bind(component) + }); + $(element).disableSelection(); + }, + + onSortUpdate : function (event, ui) + { + var productId = ui.item.attr('data-product-id'); + var position = 1; + var products = this.products(); + var editPositions = this.editPositions(); + + var previousProductId = ui.item.prev('li.product-list-item').attr('data-product-id'); + if (previousProductId !== undefined) { + var previousProduct = this.getProductById(previousProductId); + position = parseInt(previousProduct.getPosition(), 10) + 1; + } + + this.getProductById(productId).setPosition(position); + editPositions[productId] = position; + + ui.item.nextAll('li.product-list-item').each(function (index, element) { + var currentProduct = this.getProductById(element.getAttribute('data-product-id')); + if(currentProduct.getPosition()) { + position = position + 1; + currentProduct.setPosition(position); + editPositions[currentProduct.getId()] = position; + } + }.bind(this)) + + this.products(this.sortProduct(products)); + this.editPositions(editPositions); + }, + + toggleSortType: function (product) { + var products = this.products(); + var editPositions = this.editPositions(); + + if (product.getPosition() !== undefined) { + var lastProduct = products[products.length -1]; + if (lastProduct.hasPosition() || lastProduct.getScore() >= product.getScore()) { + this.refreshProductList(); + } + product.setPosition(undefined); + if (editPositions[product.getId()]) { + delete editPositions[product.getId()]; + } + } else { + var allPositions = products + .filter(function (product) { return product.hasPosition(); }) + .map(function (product) { return product.getPosition(); }) + .concat([0]); + + var maxPosition = Math.max.apply(null, allPositions); + editPositions[product.getId()] = maxPosition + 1; + product.setPosition(maxPosition + 1); + } + + this.products(this.sortProduct(products)); + this.editPositions(editPositions); + } + }); +}); diff --git a/src/module-elasticsuite-catalog/view/adminhtml/web/js/form/element/product-sorter/item.js b/src/module-elasticsuite-catalog/view/adminhtml/web/js/form/element/product-sorter/item.js new file mode 100644 index 000000000..ae9633855 --- /dev/null +++ b/src/module-elasticsuite-catalog/view/adminhtml/web/js/form/element/product-sorter/item.js @@ -0,0 +1,70 @@ +/** + * DISCLAIMER + * + * Do not edit or add to this file if you wish to upgrade Smile Elastic Suite to newer + * versions in the future. + * + * + * @category Smile + * @package Smile\ElasticsuiteCatalog + * @author Aurelien FOUCRET + * @copyright 2016 Smile + * @license Open Software License ("OSL") v. 3.0 + */ + +define([ + 'uiComponent', + 'jquery', + 'Magento_Catalog/js/price-utils', + 'mage/translate' +], function (Component, $, priceUtil) { + + 'use strict'; + + + return Component.extend({ + initialize : function () { + this._super(); + this.observe(['position']); + this.setPosition(this.data.position); + }, + + setPosition : function (position) { + if (position) { + position = parseInt(position, 10); + } + + this.position(position); + }, + + compareTo : function(product) { + var result = 0; + result = this.hasPosition() && product.hasPosition() ? this.getPosition() - product.getPosition() : 0; + result = result === 0 && this.hasPosition() ? -1 : result; + result = result === 0 && product.hasPosition() ? 1 : result; + result = result === 0 ? product.getScore() - this.getScore() : result; + result = result === 0 ? product.getId() - this.getId(): result; + + return result; + }, + + getPosition : function () { return this.position(); }, + + hasPosition : function () { return this.getPosition() !== undefined && this.getPosition() !== null; }, + + getFormattedPrice : function () { return priceUtil.formatPrice(this.data.price, this.data.priceFormat); }, + + getId : function () { return parseInt(this.data.id, 10); }, + + getScore : function () { return parseFloat(this.data.score); }, + + getImageUrl : function () { return this.data.image; }, + + getName : function () { return this.data.name; }, + + getIsInStock : function () { return Boolean(this.data['is_in_stock']) }, + + getStockLabel : function () { return this.getIsInStock() === true ? $.mage.__('In Stock') : $.mage.__('Out Of Stock'); } + }); + +}); diff --git a/src/module-elasticsuite-catalog/view/adminhtml/web/template/catalog/product/form/renderer/sort.html b/src/module-elasticsuite-catalog/view/adminhtml/web/template/catalog/product/form/renderer/sort.html deleted file mode 100644 index af4c869fb..000000000 --- a/src/module-elasticsuite-catalog/view/adminhtml/web/template/catalog/product/form/renderer/sort.html +++ /dev/null @@ -1,35 +0,0 @@ - - -
-

-
- -
-
    -
  • - -
    - -

    - -
    - -
    -

    -

    -
    - -
    - - -
    - -
  • -
- -

- -

-
\ No newline at end of file diff --git a/src/module-elasticsuite-catalog/view/adminhtml/web/template/form/element/product-sorter.html b/src/module-elasticsuite-catalog/view/adminhtml/web/template/form/element/product-sorter.html new file mode 100644 index 000000000..69e52659d --- /dev/null +++ b/src/module-elasticsuite-catalog/view/adminhtml/web/template/form/element/product-sorter.html @@ -0,0 +1,52 @@ + + +
+ +
+
+
+ + + + + +
+
+
+
+
+ +
+ +
    +
  • +
    + +

    + +
    + +
    +

    +

    +
    + +
    + + +
    +
  • +
+ + +
+
diff --git a/src/module-elasticsuite-catalog/view/frontend/layout/catalog_category_view_type_layered.xml b/src/module-elasticsuite-catalog/view/frontend/layout/catalog_category_view_type_layered.xml index 8bda1c8f2..38e81b2f2 100644 --- a/src/module-elasticsuite-catalog/view/frontend/layout/catalog_category_view_type_layered.xml +++ b/src/module-elasticsuite-catalog/view/frontend/layout/catalog_category_view_type_layered.xml @@ -17,7 +17,7 @@ - + - + -
+
+ <%- data.breadcrumb %> <%- data.title %>
diff --git a/src/module-elasticsuite-core/Index/Indices/Config.php b/src/module-elasticsuite-core/Index/Indices/Config.php index fda87ad32..9d0b23c8f 100644 --- a/src/module-elasticsuite-core/Index/Indices/Config.php +++ b/src/module-elasticsuite-core/Index/Indices/Config.php @@ -18,6 +18,9 @@ use Magento\Framework\Config\CacheInterface; use Magento\Framework\ObjectManagerInterface; use Smile\ElasticsuiteCore\Api\Index\Mapping\DynamicFieldProviderInterface; +use Smile\ElasticsuiteCore\Api\Index\TypeInterfaceFactory as TypeFactory; +use Smile\ElasticsuiteCore\Api\Index\MappingInterfaceFactory as MappingFactory; +use Smile\ElasticsuiteCore\Api\Index\Mapping\FieldInterfaceFactory as MappingFieldFactory; /** * ElasticSuite indices configuration; @@ -68,19 +71,28 @@ class Config extends \Magento\Framework\Config\Data /** * Instanciate config. * - * @param Reader $reader Config file reader. - * @param CacheInterface $cache Cache instance. - * @param ObjectManagerInterface $objectManager Object manager (used to instanciate several factories) - * @param string $cacheId Default config cache id. + * @param Reader $reader Config file reader. + * @param CacheInterface $cache Cache instance. + * @param ObjectManagerInterface $objectManager Object manager (used to instanciate several factories) + * @param TypeFactory $typeFactory Index type factory. + * @param MappingFactory $mappingFactory Index mapping factory. + * @param MappingFieldFactory $mappingFieldFactory Index mapping field factory. + * @param string $cacheId Default config cache id. */ public function __construct( Reader $reader, CacheInterface $cache, ObjectManagerInterface $objectManager, + TypeFactory $typeFactory, + MappingFactory $mappingFactory, + MappingFieldFactory $mappingFieldFactory, $cacheId = self::CACHE_ID ) { - $this->objectManager = $objectManager; - $this->initFactories(); + $this->typeFactory = $typeFactory; + $this->mappingFactory = $mappingFactory; + $this->mappingFieldFactory = $mappingFieldFactory; + $this->objectManager = $objectManager; + parent::__construct($reader, $cache, $cacheId); } @@ -95,26 +107,6 @@ protected function initData() $this->_data = array_map([$this, 'initIndexConfig'], $this->_data); } - /** - * Init factories used by the configuration to build types, mappings and fields objects. - * - * @return void - */ - private function initFactories() - { - $this->typeFactory = $this->objectManager->get( - 'Smile\ElasticsuiteCore\Api\Index\TypeInterfaceFactory' - ); - - $this->mappingFactory = $this->objectManager->get( - 'Smile\ElasticsuiteCore\Api\Index\MappingInterfaceFactory' - ); - - $this->mappingFieldFactory = $this->objectManager->get( - 'Smile\ElasticsuiteCore\Api\Index\Mapping\FieldInterfaceFactory' - ); - } - /** * Init type, mapping, and fields from a index configuration array. * diff --git a/src/module-elasticsuite-core/Model/Config/Source/FuzzinessValue.php b/src/module-elasticsuite-core/Model/Config/Source/FuzzinessValue.php new file mode 100644 index 000000000..ef68003d0 --- /dev/null +++ b/src/module-elasticsuite-core/Model/Config/Source/FuzzinessValue.php @@ -0,0 +1,37 @@ + + * @copyright 2016 Smile + * @license Open Software License ("OSL") v. 3.0 + */ + +namespace Smile\ElasticsuiteCore\Model\Config\Source; + +/** + * Fuzziness value config source model. + * + * @category Smile + * @package Smile\ElasticsuiteCore + * @author Aurelien FOUCRET + */ +class FuzzinessValue implements \Magento\Framework\Option\ArrayInterface +{ + /** + * {@inheritDoc} + */ + public function toOptionArray() + { + return [ + 'AUTO' => __('Automatic'), + '1' => 1, + '2' => 2, + ]; + } +} diff --git a/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Request/Query/Builder.php b/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Request/Query/Builder.php index 32fe5016a..0a557328c 100644 --- a/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Request/Query/Builder.php +++ b/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Request/Query/Builder.php @@ -40,6 +40,7 @@ class Builder implements BuilderInterface QueryInterface::TYPE_MATCH => 'Smile\ElasticsuiteCore\Search\Adapter\Elasticsuite\Request\Query\Builder\Match', QueryInterface::TYPE_COMMON => 'Smile\ElasticsuiteCore\Search\Adapter\Elasticsuite\Request\Query\Builder\Common', QueryInterface::TYPE_MULTIMATCH => 'Smile\ElasticsuiteCore\Search\Adapter\Elasticsuite\Request\Query\Builder\MultiMatch', + QueryInterface::TYPE_MISSING => 'Smile\ElasticsuiteCore\Search\Adapter\Elasticsuite\Request\Query\Builder\Missing', ]; /** diff --git a/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Request/Query/Builder/Missing.php b/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Request/Query/Builder/Missing.php new file mode 100644 index 000000000..3045a3ff2 --- /dev/null +++ b/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Request/Query/Builder/Missing.php @@ -0,0 +1,36 @@ + + * @copyright 2016 Smile + * @license Open Software License ("OSL") v. 3.0 + */ + +namespace Smile\ElasticsuiteCore\Search\Adapter\Elasticsuite\Request\Query\Builder; + +use Smile\ElasticsuiteCore\Search\Request\QueryInterface; +use Smile\ElasticsuiteCore\Search\Adapter\Elasticsuite\Request\Query\BuilderInterface; + +/** + * Build an ES missing field query. + * + * @category Smile + * @package Smile\ElasticsuiteCore + * @author Aurelien FOUCRET + */ +class Missing implements BuilderInterface +{ + /** + * {@inheritDoc} + */ + public function buildQuery(QueryInterface $query) + { + return ['missing' => ['field' => $query->getField()]]; + } +} diff --git a/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Response/QueryResponse.php b/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Response/QueryResponse.php index 086bdfc5c..a52bc5e1f 100644 --- a/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Response/QueryResponse.php +++ b/src/module-elasticsuite-core/Search/Adapter/Elasticsuite/Response/QueryResponse.php @@ -24,7 +24,7 @@ * @package Smile\ElasticsuiteCore * @author Aurelien FOUCRET */ -class QueryResponse implements ResponseInterface, \IteratorAggregate, \Countable +class QueryResponse implements ResponseInterface { /** * Document Collection diff --git a/src/module-elasticsuite-core/Search/Request/Aggregation/AggregationFactory.php b/src/module-elasticsuite-core/Search/Request/Aggregation/AggregationFactory.php index 9cf23d591..aeaffc413 100644 --- a/src/module-elasticsuite-core/Search/Request/Aggregation/AggregationFactory.php +++ b/src/module-elasticsuite-core/Search/Request/Aggregation/AggregationFactory.php @@ -29,25 +29,16 @@ class AggregationFactory /** * @var array */ - private $factories = [ - BucketInterface::TYPE_TERM => 'Smile\ElasticsuiteCore\Search\Request\Aggregation\Bucket\TermFactory', - BucketInterface::TYPE_HISTOGRAM => 'Smile\ElasticsuiteCore\Search\Request\Aggregation\Bucket\HistogramFactory', - BucketInterface::TYPE_QUERY_GROUP => 'Smile\ElasticsuiteCore\Search\Request\Aggregation\Bucket\QueryGroupFactory', - ]; - - /** - * @var ObjectManagerInterface - */ - private $objectManager; + private $factories; /** * Constructor. * - * @param ObjectManagerInterface $objectManager Object manager instance. + * @param array $factories Aggregation factories by type. */ - public function __construct(ObjectManagerInterface $objectManager) + public function __construct($factories = []) { - $this->objectManager = $objectManager; + $this->factories = $factories; } /** @@ -64,8 +55,6 @@ public function create($bucketType, $bucketParams) throw new \LogicException("No factory found for query of type {$bucketType}"); } - $factory = $this->objectManager->get($this->factories[$bucketType]); - - return $factory->create($bucketParams); + return $this->factories[$bucketType]->create($bucketParams); } } diff --git a/src/module-elasticsuite-core/Search/Request/Builder.php b/src/module-elasticsuite-core/Search/Request/Builder.php index fc9ca6fec..476127aaa 100644 --- a/src/module-elasticsuite-core/Search/Request/Builder.php +++ b/src/module-elasticsuite-core/Search/Request/Builder.php @@ -177,19 +177,19 @@ public function create( */ private function getSpellingType(ContainerConfigurationInterface $containerConfig, $queryText) { - $spellingType = SpellcheckerInterface::SPELLING_TYPE_EXACT; + if (is_array($queryText)) { + $queryText = implode(" ", $queryText); + } - if (!is_array($queryText)) { - $spellcheckRequestParams = [ - 'index' => $containerConfig->getIndexName(), - 'type' => $containerConfig->getTypeName(), - 'queryText' => $queryText, - 'cutoffFrequency' => $containerConfig->getRelevanceConfig()->getCutOffFrequency(), - ]; + $spellcheckRequestParams = [ + 'index' => $containerConfig->getIndexName(), + 'type' => $containerConfig->getTypeName(), + 'queryText' => $queryText, + 'cutoffFrequency' => $containerConfig->getRelevanceConfig()->getCutOffFrequency(), + ]; - $spellcheckRequest = $this->spellcheckRequestFactory->create($spellcheckRequestParams); - $spellingType = $this->spellchecker->getSpellingType($spellcheckRequest); - } + $spellcheckRequest = $this->spellcheckRequestFactory->create($spellcheckRequestParams); + $spellingType = $this->spellchecker->getSpellingType($spellcheckRequest); return $spellingType; } diff --git a/src/module-elasticsuite-core/Search/Request/Query/Fulltext/QueryBuilder.php b/src/module-elasticsuite-core/Search/Request/Query/Fulltext/QueryBuilder.php index 37c74a481..430c0159d 100644 --- a/src/module-elasticsuite-core/Search/Request/Query/Fulltext/QueryBuilder.php +++ b/src/module-elasticsuite-core/Search/Request/Query/Fulltext/QueryBuilder.php @@ -62,7 +62,7 @@ public function create(ContainerConfigurationInterface $containerConfig, $queryT $fuzzySpellingTypes = [SpellcheckerInterface::SPELLING_TYPE_FUZZY, SpellcheckerInterface::SPELLING_TYPE_MOST_FUZZY]; if ($spellingType == SpellcheckerInterface::SPELLING_TYPE_PURE_STOPWORDS) { - $query = $this->getPurewordsQuery($containerConfig, $queryText, $boost); + $query = $this->getPureStopwordsQuery($containerConfig, $queryText, $boost); } elseif (in_array($spellingType, $fuzzySpellingTypes)) { $query = $this->getSpellcheckedQuery($containerConfig, $queryText, $spellingType, $boost); } @@ -147,7 +147,7 @@ private function getWeightedSearchQuery(ContainerConfigurationInterface $contain * * @return QueryInterface */ - private function getPurewordsQuery(ContainerConfigurationInterface $containerConfig, $queryText, $boost) + private function getPureStopwordsQuery(ContainerConfigurationInterface $containerConfig, $queryText, $boost) { $relevanceConfig = $containerConfig->getRelevanceConfig(); diff --git a/src/module-elasticsuite-core/Search/Request/Query/Missing.php b/src/module-elasticsuite-core/Search/Request/Query/Missing.php new file mode 100644 index 000000000..8aad2f384 --- /dev/null +++ b/src/module-elasticsuite-core/Search/Request/Query/Missing.php @@ -0,0 +1,93 @@ + + * @copyright 2016 Smile + * @license Open Software License ("OSL") v. 3.0 + */ + +namespace Smile\ElasticsuiteCore\Search\Request\Query; + +use Smile\ElasticsuiteCore\Search\Request\QueryInterface; + +/** + * Missing field definition implementation. + * + * @category Smile + * @package Smile\ElasticsuiteCore + * @author Aurelien FOUCRET + */ +class Missing implements QueryInterface +{ + /** + * @var string + */ + private $name; + + /** + * @var string + */ + private $field; + + /** + * @var integer + */ + private $boost; + + /** + * Constructor. + * + * @param string $field Query field. + * @param string $name Query name. + * @param integer $boost Query boost. + */ + public function __construct( + $field, + $name = null, + $boost = QueryInterface::DEFAULT_BOOST_VALUE + ) { + $this->name = $name; + $this->boost = $boost; + $this->field = $field; + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return $this->name; + } + + /** + * {@inheritDoc} + */ + public function getBoost() + { + return $this->boost; + } + + /** + * {@inheritDoc} + */ + public function getType() + { + return QueryInterface::TYPE_MISSING; + } + + /** + * Negated query. + * + * @return string + */ + public function getField() + { + return $this->field; + } +} diff --git a/src/module-elasticsuite-core/Search/Request/Query/QueryFactory.php b/src/module-elasticsuite-core/Search/Request/Query/QueryFactory.php index dbc5fe9aa..58aac63ba 100644 --- a/src/module-elasticsuite-core/Search/Request/Query/QueryFactory.php +++ b/src/module-elasticsuite-core/Search/Request/Query/QueryFactory.php @@ -28,32 +28,16 @@ class QueryFactory /** * @var array */ - private $factories = [ - QueryInterface::TYPE_BOOL => 'Smile\ElasticsuiteCore\Search\Request\Query\BooleanFactory', - QueryInterface::TYPE_FILTER => 'Smile\ElasticsuiteCore\Search\Request\Query\FilteredFactory', - QueryInterface::TYPE_NESTED => 'Smile\ElasticsuiteCore\Search\Request\Query\NestedFactory', - QueryInterface::TYPE_NOT => 'Smile\ElasticsuiteCore\Search\Request\Query\NotFactory', - QueryInterface::TYPE_TERM => 'Smile\ElasticsuiteCore\Search\Request\Query\TermFactory', - QueryInterface::TYPE_TERMS => 'Smile\ElasticsuiteCore\Search\Request\Query\TermsFactory', - QueryInterface::TYPE_RANGE => 'Smile\ElasticsuiteCore\Search\Request\Query\RangeFactory', - QueryInterface::TYPE_MATCH => 'Smile\ElasticsuiteCore\Search\Request\Query\MatchFactory', - QueryInterface::TYPE_COMMON => 'Smile\ElasticsuiteCore\Search\Request\Query\CommonFactory', - QueryInterface::TYPE_MULTIMATCH => 'Smile\ElasticsuiteCore\Search\Request\Query\MultiMatchFactory', - ]; - - /** - * @var ObjectManagerInterface - */ - private $objectManager; + private $factories; /** * Constructor. * - * @param ObjectManagerInterface $objectManager Object manager. + * @param array $factories Query factories by type. */ - public function __construct(ObjectManagerInterface $objectManager) + public function __construct($factories = []) { - $this->objectManager = $objectManager; + $this->factories = $factories; } /** @@ -70,8 +54,6 @@ public function create($queryType, $queryParams) throw new \LogicException("No factory found for query of type {$queryType}"); } - $factory = $this->objectManager->get($this->factories[$queryType]); - - return $factory->create($queryParams); + return $this->factories[$queryType]->create($queryParams); } } diff --git a/src/module-elasticsuite-core/Search/Request/QueryInterface.php b/src/module-elasticsuite-core/Search/Request/QueryInterface.php index 67c497be6..b25708b3f 100644 --- a/src/module-elasticsuite-core/Search/Request/QueryInterface.php +++ b/src/module-elasticsuite-core/Search/Request/QueryInterface.php @@ -32,4 +32,5 @@ interface QueryInterface extends \Magento\Framework\Search\Request\QueryInterfac const TYPE_NOT = 'notQuery'; const TYPE_MULTIMATCH = 'multiMatchQuery'; const TYPE_COMMON = 'commonQuery'; + const TYPE_MISSING = 'missingQuery'; } diff --git a/src/module-elasticsuite-core/composer.json b/src/module-elasticsuite-core/composer.json index 5871efa03..a6e928090 100644 --- a/src/module-elasticsuite-core/composer.json +++ b/src/module-elasticsuite-core/composer.json @@ -20,14 +20,14 @@ ], "require": { "php": "~5.5.0|~5.6.0|~7.0.0", - "magento/framework": "*", - "magento/module-store": "*", - "magento/module-backend": "*", - "magento/module-search": "*", - "magento/module-catalog": "*", - "magento/module-catalog-search": "*", + "magento/framework": ">=100.1.0", + "magento/module-store": ">=100.1.0", + "magento/module-backend": ">=100.1.0", + "magento/module-search": ">=100.1.0", + "magento/module-catalog": ">=100.1.0", + "magento/module-catalog-search": ">=100.1.0", "magento/magento-composer-installer": "*", - "elasticsearch/elasticsearch": "2.1.*" + "elasticsearch/elasticsearch": "^2.2" }, "version": "2.1.0", "autoload": { diff --git a/src/module-elasticsuite-core/etc/adminhtml/elasticsuite_relevance.xml b/src/module-elasticsuite-core/etc/adminhtml/elasticsuite_relevance.xml index fa6c44c23..b66066e5f 100644 --- a/src/module-elasticsuite-core/etc/adminhtml/elasticsuite_relevance.xml +++ b/src/module-elasticsuite-core/etc/adminhtml/elasticsuite_relevance.xml @@ -71,13 +71,14 @@ Magento\Config\Model\Config\Source\Yesno - + + Smile\ElasticsuiteCore\Model\Config\Source\FuzzinessValue 1 - here for more information.]]> + here for more information.]]> diff --git a/src/module-elasticsuite-core/etc/di.xml b/src/module-elasticsuite-core/etc/di.xml index ee7a4266e..1ce32d569 100644 --- a/src/module-elasticsuite-core/etc/di.xml +++ b/src/module-elasticsuite-core/etc/di.xml @@ -56,6 +56,34 @@ + + + + Smile\ElasticsuiteCore\Search\Request\Query\BooleanFactory + Smile\ElasticsuiteCore\Search\Request\Query\FilteredFactory + Smile\ElasticsuiteCore\Search\Request\Query\NestedFactory + Smile\ElasticsuiteCore\Search\Request\Query\NotFactory + Smile\ElasticsuiteCore\Search\Request\Query\MissingFactory + Smile\ElasticsuiteCore\Search\Request\Query\TermFactory + Smile\ElasticsuiteCore\Search\Request\Query\TermsFactory + Smile\ElasticsuiteCore\Search\Request\Query\RangeFactory + Smile\ElasticsuiteCore\Search\Request\Query\MatchFactory + Smile\ElasticsuiteCore\Search\Request\Query\CommonFactory + Smile\ElasticsuiteCore\Search\Request\Query\MultiMatchFactory + + + + + + + + Smile\ElasticsuiteCore\Search\Request\Aggregation\Bucket\TermFactory + Smile\ElasticsuiteCore\Search\Request\Aggregation\Bucket\HistogramFactory + Smile\ElasticsuiteCore\Search\Request\Aggregation\Bucket\QueryGroupFactory + + + + @@ -319,4 +347,12 @@ + + + + + Magento\AdvancedSearch\Model\DataProvider\Suggestions + + +
diff --git a/src/module-elasticsuite-core/view/base/requirejs-config.js b/src/module-elasticsuite-core/view/base/requirejs-config.js deleted file mode 100644 index c7e83c853..000000000 --- a/src/module-elasticsuite-core/view/base/requirejs-config.js +++ /dev/null @@ -1,21 +0,0 @@ -/** - * DISCLAIMER - * - * Do not edit or add to this file if you wish to upgrade Smile Elastic Suite to newer - * versions in the future. - * - * - * @category Smile - * @package Smile\ElasticsuiteCatalog - * @author Aurelien FOUCRET - * @copyright 2016 Smile - * @license Open Software License ("OSL") v. 3.0 - */ - -var config = { - map: { - '*': { - 'Smile.ES.FormListener': 'Smile_ElasticsuiteCore/js/form-listener' - } - } -}; diff --git a/src/module-elasticsuite-core/view/base/web/js/form-listener.js b/src/module-elasticsuite-core/view/base/web/js/form-listener.js deleted file mode 100644 index 2ee77a134..000000000 --- a/src/module-elasticsuite-core/view/base/web/js/form-listener.js +++ /dev/null @@ -1,85 +0,0 @@ -define(["jquery"], function ($) { - - var FormListener = function (formId, formChangedEvent, listenFormElements) { - this.form = $('#' + formId); - this.formChangedEvent = formChangedEvent; - this.listenFormElements = listenFormElements; - }; - - FormListener.prototype.startListener = function () { - if (this.timer === undefined || this.timer === null) { - this.hash = this.getFormHash(); - this.timer = setInterval(this.detectChanges.bind(this), 1000); - } - }; - - FormListener.prototype.stopListener = function () { - if (this.timer === undefined || this.timer === null) { - clearInterval(this.timer); - delete this.timer; - } - }; - - FormListener.prototype.getFormHash = function () { - var serializedElements = this.form.serializeArray(); - - var filterElementFunction = this.getFilterFunction(); - if (filterElementFunction) { - serializedElements = serializedElements.filter(filterElementFunction); - } - - return serializedElements.map(function (formElement) { return formElement.name + formElement.value; }).join('|'); - } - - FormListener.prototype.getFilterFunction = function () { - var filterFunction = null; - - var isElementTargeted = function (elementName, targetName) { - var isElementTargeted = elementName === targetName; - if (targetName.match(/.*\[.*\]$/)) { - isElementTargeted = elementName.startsWith(targetName); - } - return isElementTargeted; - } - - if (Object.prototype.toString.call(this.listenFormElements) === '[object Array]') { - filterFunction = function(element) { - var addElement = false; - for (var i = 0; i < this.listenFormElements.length; i++) { - addElement = addElement || isElementTargeted(element.name, this.listenFormElements[i]); - } - return addElement; - }; - } else if (typeof this.listenFormElements === 'string') { - filterFunction = function (element) { - return isElementTargeted(element.name, this.listenFormElements); - }; - } else if (typeof this.listenFormElements === 'object') { - filterFunction = function (element) { - return this.listenFormElements.name === element.name; - }; - } - - return filterFunction.bind(this); - }; - - FormListener.prototype.detectChanges = function () { - var currentHash = this.getFormHash(); - - if (currentHash !== this.hash) { - $(document).trigger(this.formChangedEvent, [this.form]); - } - - this.hash = currentHash; - } - - FormListener.prototype.serialize = function () { - return this.form.serialize(); - } - - FormListener.prototype.serializeArray = function () { - return this.form.serializeArray(); - } - - return FormListener; -}); \ No newline at end of file diff --git a/src/module-elasticsuite-core/view/frontend/web/css/source/_module.less b/src/module-elasticsuite-core/view/frontend/web/css/source/_module.less index b1237571d..47a109f17 100644 --- a/src/module-elasticsuite-core/view/frontend/web/css/source/_module.less +++ b/src/module-elasticsuite-core/view/frontend/web/css/source/_module.less @@ -22,12 +22,12 @@ .lib-list-reset-styles(); dt { &:not(:empty) { - .lib-css(background, @autocomplete-background); - .lib-css(border, @autocomplete-border); + .lib-css(background, @autocomplete__background-color); + .lib-css(border, @autocomplete__border); border-top: 0; border-bottom: 0; } - .lib-css(border-top, @autocomplete-item-border); + .lib-css(border-top, @autocomplete-item__border); cursor: default; margin: 0; padding: @indent__xs @indent__xl @indent__xs @indent__s; @@ -39,16 +39,16 @@ } &:hover, &.selected { - .lib-css(background, @autocomplete-item-hover); + .lib-css(background, @autocomplete-item__hover__color); } } dd { &:not(:empty) { - .lib-css(background, @autocomplete-background); - .lib-css(border, @autocomplete-border); + .lib-css(background, @autocomplete__background-color); + .lib-css(border, @autocomplete__border); border-top: 0; } - .lib-css(border-top, @autocomplete-item-border); + .lib-css(border-top, @autocomplete-item__border); cursor: pointer; margin: 0; padding: @indent__xs @indent__s; @@ -60,10 +60,10 @@ } &:hover, &.selected { - .lib-css(background, @autocomplete-item-hover); + .lib-css(background, @autocomplete-item__hover__color); } .amount { - .lib-css(color, @autocomplete-item-amount-color); + .lib-css(color, @autocomplete-item-amount__color); position: absolute; right: 7px; top: @indent__xs; diff --git a/src/module-elasticsuite-core/view/frontend/web/js/form-mini.js b/src/module-elasticsuite-core/view/frontend/web/js/form-mini.js index 91b071e41..f33db6c50 100644 --- a/src/module-elasticsuite-core/view/frontend/web/js/form-mini.js +++ b/src/module-elasticsuite-core/view/frontend/web/js/form-mini.js @@ -5,7 +5,7 @@ define([ 'underscore', 'mage/template', 'Magento_Catalog/js/price-utils', - 'Magento_Ui/js/lib/ko/template/loader', + 'Magento_Ui/js/lib/knockout/template/loader', 'jquery/ui', 'mage/translate', 'mageQuickSearch' @@ -29,6 +29,7 @@ define([ */ _create: function () { this.templateCache = []; + this.currentRequest = null; this._initTemplates(); this._super(); }, @@ -198,64 +199,73 @@ define([ this.submitBtn.disabled = this._isEmpty(value); if (value.length >= parseInt(this.options.minSearchLength, 10)) { - $.get(this.options.url, {q: value}, $.proxy(function (data) { - var self = this; - var lastElement = false; - var content = this._getResultWrapper(); - var sectionDropdown = this._getSectionHeader(); - $.each(data, function(index, element) { - - if (!lastElement || (lastElement && lastElement.type !== element.type)) { - sectionDropdown = this._getSectionHeader(element.type); - } - var elementHtml = this._renderItem(element, index); + this.currentRequest = $.ajax({ + method: "GET", + url: this.options.url, + data:{q: value}, + // This function will ensure proper killing of the last Ajax call. + // In order to prevent requests of an old request to pop up later and replace results. + beforeSend: function() { if (this.currentRequest !== null) { this.currentRequest.abort(); }}.bind(this), + success: $.proxy(function (data) { + var self = this; + var lastElement = false; + var content = this._getResultWrapper(); + var sectionDropdown = this._getSectionHeader(); + $.each(data, function(index, element) { - sectionDropdown.append(elementHtml); + if (!lastElement || (lastElement && lastElement.type !== element.type)) { + sectionDropdown = this._getSectionHeader(element.type); + } - if (!lastElement || (lastElement && lastElement.type !== element.type)) { - content.append(sectionDropdown); - } + var elementHtml = this._renderItem(element, index); - lastElement = element; - }.bind(this)); - this.responseList.indexList = this.autoComplete.html(content) - .css(clonePosition) - .show() - .find(this.options.responseFieldElements + ':visible'); - - this._resetResponseList(false); - this.element.removeAttr('aria-activedescendant'); - - if (this.responseList.indexList.length) { - this._updateAriaHasPopup(true); - } else { - this._updateAriaHasPopup(false); - } - - this.responseList.indexList - .on('click', function (e) { - self.responseList.selected = $(this); - if (self.responseList.selected.attr("href")) { - window.location.href = self.responseList.selected.attr("href"); - e.stopPropagation(); - return false; - } - self.searchForm.trigger('submit'); - }) - .on('mouseenter mouseleave', function (e) { - self.responseList.indexList.removeClass(self.options.selectClass); - $(this).addClass(self.options.selectClass); - self.responseList.selected = $(e.target); - self.element.attr('aria-activedescendant', $(e.target).attr('id')); - }) - .on('mouseout', function () { - if (!self._getLastElement() && self._getLastElement().hasClass(self.options.selectClass)) { - $(this).removeClass(self.options.selectClass); - self._resetResponseList(false); + sectionDropdown.append(elementHtml); + + if (!lastElement || (lastElement && lastElement.type !== element.type)) { + content.append(sectionDropdown); } - }); - }, this)); + + lastElement = element; + }.bind(this)); + this.responseList.indexList = this.autoComplete.html(content) + .css(clonePosition) + .show() + .find(this.options.responseFieldElements + ':visible'); + + this._resetResponseList(false); + this.element.removeAttr('aria-activedescendant'); + + if (this.responseList.indexList.length) { + this._updateAriaHasPopup(true); + } else { + this._updateAriaHasPopup(false); + } + + this.responseList.indexList + .on('click', function (e) { + self.responseList.selected = $(this); + if (self.responseList.selected.attr("href")) { + window.location.href = self.responseList.selected.attr("href"); + e.stopPropagation(); + return false; + } + self.searchForm.trigger('submit'); + }) + .on('mouseenter mouseleave', function (e) { + self.responseList.indexList.removeClass(self.options.selectClass); + $(this).addClass(self.options.selectClass); + self.responseList.selected = $(e.target); + self.element.attr('aria-activedescendant', $(e.target).attr('id')); + }) + .on('mouseout', function () { + if (!self._getLastElement() && self._getLastElement().hasClass(self.options.selectClass)) { + $(this).removeClass(self.options.selectClass); + self._resetResponseList(false); + } + }); + },this) + }); } else { this._resetResponseList(true); this.autoComplete.hide(); diff --git a/src/module-elasticsuite-swatches/Helper/Swatches.php b/src/module-elasticsuite-swatches/Helper/Swatches.php index e33dccf62..9d1afd3b6 100644 --- a/src/module-elasticsuite-swatches/Helper/Swatches.php +++ b/src/module-elasticsuite-swatches/Helper/Swatches.php @@ -12,6 +12,7 @@ */ namespace Smile\ElasticsuiteSwatches\Helper; +use Magento\Catalog\Api\Data\ProductInterface as Product; use Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection; /** @@ -26,28 +27,47 @@ class Swatches extends \Magento\Swatches\Helper\Data { /** + * @SuppressWarnings(PHPMD.ElseExpression) * {@inheritDoc} */ - public function loadVariationByFallback($parentProduct, array $attributes) + public function loadVariationByFallback(Product $parentProduct, array $attributes) { - $parentProduct = $this->createSwatchProduct($parentProduct); - if (! $parentProduct) { - return false; - } + $variation = false; + + if ($this->isProductHasSwatch($parentProduct) && $parentProduct->getDocumentSource() !== null) { + $documentSource = $parentProduct->getDocumentSource(); + $childrenIds = isset($documentSource['children_ids']) ? $documentSource['children_ids'] : []; + + if (!empty($childrenIds)) { + $childrenIds = array_map('intval', $childrenIds); + + $productCollection = $this->productCollectionFactory->create(); + $productCollection->addIdFilter($childrenIds); - $productCollection = $this->productCollectionFactory->create(); - $this->addFilterByParent($productCollection, $parentProduct->getId()); + $configurableAttributes = $this->getAttributesFromConfigurable($parentProduct); + $allAttributesArray = []; - $resultAttributesToFilter = $attributes; + foreach ($configurableAttributes as $attribute) { + foreach ($attribute->getOptions() as $option) { + $allAttributesArray[$attribute['attribute_code']][] = (int) $option->getValue(); + } + } - $this->addFilterByAttributes($productCollection, $resultAttributesToFilter); + $resultAttributesToFilter = array_merge($attributes, array_diff_key($allAttributesArray, $attributes)); - $variationProduct = $productCollection->getFirstItem(); - if ($variationProduct && $variationProduct->getId()) { - return $this->productRepository->getById($variationProduct->getId()); + $this->addFilterByAttributes($productCollection, $resultAttributesToFilter); + + $variationProduct = $productCollection->getFirstItem(); + + if ($variationProduct && $variationProduct->getId()) { + $variation = $this->productRepository->getById($variationProduct->getId()); + } + } + } else { + $variation = parent::loadVariationByFallback($parentProduct, $attributes); } - return false; + return $variation; } /** diff --git a/src/module-elasticsuite-swatches/Model/Plugin/ProductImage.php b/src/module-elasticsuite-swatches/Model/Plugin/ProductImage.php index 8dedcb57c..44474c763 100644 --- a/src/module-elasticsuite-swatches/Model/Plugin/ProductImage.php +++ b/src/module-elasticsuite-swatches/Model/Plugin/ProductImage.php @@ -51,8 +51,13 @@ protected function getFilterArray(array $request) foreach ($request as $code => $value) { if (in_array($code, $attributeCodes)) { $attribute = $this->eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $code); + + if (isset($filterArray[$code]) && !is_array($filterArray[$code])) { + $filterArray[$code] = [$filterArray[$code]]; + } + if ($attribute->getId() && $this->canReplaceImageWithSwatch($attribute)) { - $filterArray[$code] = $this->getOptionIds($attribute, $value); + $filterArray[$code][] = $this->getOptionIds($attribute, $value); } } } @@ -81,7 +86,7 @@ private function getOptionIds(Attribute $attribute, $labels) foreach ($labels as $label) { foreach ($options as $option) { if ($option['label'] == $label) { - $optionIds[] = $option['value']; + $optionIds[] = (int) $option['value']; } } } diff --git a/src/module-elasticsuite-swatches/composer.json b/src/module-elasticsuite-swatches/composer.json index 9990d23ba..b3604ab40 100644 --- a/src/module-elasticsuite-swatches/composer.json +++ b/src/module-elasticsuite-swatches/composer.json @@ -20,12 +20,12 @@ ], "require": { "php": "~5.5.0|~5.6.0|~7.0.0", - "magento/framework": "*", - "magento/module-store": "*", - "magento/module-backend": "*", - "magento/module-search": "*", - "magento/module-catalog": "*", - "magento/module-catalog-search": "*", + "magento/framework": ">=100.1.0", + "magento/module-store": ">=100.1.0", + "magento/module-backend": ">=100.1.0", + "magento/module-search": ">=100.1.0", + "magento/module-catalog": ">=100.1.0", + "magento/module-catalog-search": ">=100.1.0", "magento/magento-composer-installer": "*", "elasticsearch/elasticsearch": "2.1.*" }, diff --git a/src/module-elasticsuite-thesaurus/Api/Data/ThesaurusInterface.php b/src/module-elasticsuite-thesaurus/Api/Data/ThesaurusInterface.php index 714f5cc24..a6b3fb638 100644 --- a/src/module-elasticsuite-thesaurus/Api/Data/ThesaurusInterface.php +++ b/src/module-elasticsuite-thesaurus/Api/Data/ThesaurusInterface.php @@ -46,6 +46,11 @@ interface ThesaurusInterface */ const TYPE = 'type'; + /** + * Constant for field is_active + */ + const IS_ACTIVE = 'is_active'; + /** * Name of the link thesaurus/expansion terms TABLE */ @@ -99,6 +104,13 @@ public function getType(); */ public function getStoreIds(); + /** + * Get Thesaurus status + * + * @return bool + */ + public function isActive(); + /** * Set Thesaurus ID * @@ -134,4 +146,13 @@ public function setType($type); * @return ThesaurusInterface */ public function setStoreIds($storeIds); + + /** + * Set Thesaurus status + * + * @param bool $status The thesaurus status + * + * @return ThesaurusInterface + */ + public function setIsActive($status); } diff --git a/src/module-elasticsuite-thesaurus/Block/Adminhtml/Thesaurus/Edit/Form.php b/src/module-elasticsuite-thesaurus/Block/Adminhtml/Thesaurus/Edit/Form.php index 998718f13..d183c43b6 100644 --- a/src/module-elasticsuite-thesaurus/Block/Adminhtml/Thesaurus/Edit/Form.php +++ b/src/module-elasticsuite-thesaurus/Block/Adminhtml/Thesaurus/Edit/Form.php @@ -22,6 +22,8 @@ /** * Thesaurus Edit form * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * * @category Smile * @package Smile\ElasticsuiteThesaurus * @author Romain Ruaud @@ -33,24 +35,32 @@ class Form extends \Magento\Backend\Block\Widget\Form\Generic */ private $systemStore; + /** + * @var Yesno + */ + private $booleanSource; /** * Constructor * - * @param Context $context Application context - * @param \Magento\Framework\Registry $registry The registry - * @param FormFactory $formFactory Form factory - * @param Store $systemStore Store Provider - * @param array $data Object data + * @param Context $context Application context + * @param \Magento\Framework\Registry $registry The registry + * @param FormFactory $formFactory Form factory + * @param Store $systemStore Store Provider + * @param Yesno $booleanSource Boolean Input Source Model + * @param array $data Object data */ public function __construct( Context $context, Registry $registry, FormFactory $formFactory, Store $systemStore, + Yesno $booleanSource, array $data = [] ) { $this->systemStore = $systemStore; + $this->booleanSource = $booleanSource; + parent::__construct($context, $registry, $formFactory, $data); } @@ -131,6 +141,17 @@ private function initBaseFields($fieldset, $model) ] ); + $fieldset->addField( + 'is_active', + 'select', + [ + 'name' => 'is_active', + 'label' => __('Active'), + 'title' => __('Active'), + 'values' => $this->booleanSource->toOptionArray(), + ] + ); + if (!$this->_storeManager->isSingleStoreMode()) { $field = $fieldset->addField( 'store_id', diff --git a/src/module-elasticsuite-thesaurus/Model/ResourceModel/Indexer/Thesaurus.php b/src/module-elasticsuite-thesaurus/Model/ResourceModel/Indexer/Thesaurus.php index 380907f44..d46bf154a 100644 --- a/src/module-elasticsuite-thesaurus/Model/ResourceModel/Indexer/Thesaurus.php +++ b/src/module-elasticsuite-thesaurus/Model/ResourceModel/Indexer/Thesaurus.php @@ -56,7 +56,7 @@ public function getExpansions($storeId) $select = $this->getBaseSelect($storeId, ThesaurusInterface::TYPE_EXPANSION); $select->join( - ['expanded_terms' => ThesaurusInterface::REFERENCE_TABLE_NAME], + ['expanded_terms' => $this->getTable(ThesaurusInterface::REFERENCE_TABLE_NAME)], 'expanded_terms.term_id = terms.term_id AND expanded_terms.thesaurus_id = terms.thesaurus_id', [] ); @@ -80,10 +80,11 @@ private function getBaseSelect($storeId, $type) $select = $connection->select(); $select->from(['thesaurus' => $this->getMainTable()], []) - ->join(['terms' => ThesaurusInterface::EXPANSION_TABLE_NAME], 'thesaurus.thesaurus_id = terms.thesaurus_id', []) - ->join(['store' => ThesaurusInterface::STORE_TABLE_NAME], 'store.thesaurus_id = thesaurus.thesaurus_id', []) + ->join(['terms' => $this->getTable(ThesaurusInterface::EXPANSION_TABLE_NAME)], 'thesaurus.thesaurus_id = terms.thesaurus_id', []) + ->join(['store' => $this->getTable(ThesaurusInterface::STORE_TABLE_NAME)], 'store.thesaurus_id = thesaurus.thesaurus_id', []) ->group(['thesaurus.thesaurus_id', 'terms.term_id']) ->where("thesaurus.type = ?", $type) + ->where('thesaurus.is_active = 1') ->where('store.store_id IN (?)', [0, $storeId]); return $select; diff --git a/src/module-elasticsuite-thesaurus/Model/ResourceModel/Thesaurus.php b/src/module-elasticsuite-thesaurus/Model/ResourceModel/Thesaurus.php index 3b0ce3fd3..82ede6781 100644 --- a/src/module-elasticsuite-thesaurus/Model/ResourceModel/Thesaurus.php +++ b/src/module-elasticsuite-thesaurus/Model/ResourceModel/Thesaurus.php @@ -159,8 +159,12 @@ private function saveStoreRelation(\Magento\Framework\Model\AbstractModel $objec $deleteCondition[ThesaurusInterface::STORE_ID . " NOT IN (?)"] = array_keys($storeIds); - $this->getConnection()->delete(ThesaurusInterface::STORE_TABLE_NAME, $deleteCondition); - $this->getConnection()->insertOnDuplicate(ThesaurusInterface::STORE_TABLE_NAME, $storeLinks, array_keys(current($storeLinks))); + $this->getConnection()->delete($this->getTable(ThesaurusInterface::STORE_TABLE_NAME), $deleteCondition); + $this->getConnection()->insertOnDuplicate( + $this->getTable(ThesaurusInterface::STORE_TABLE_NAME), + $storeLinks, + array_keys(current($storeLinks)) + ); } } @@ -206,7 +210,7 @@ private function saveTermsRelation(\Magento\Framework\Model\AbstractModel $objec // Saves expansion terms for a thesaurus. Expansion terms are used by expansion AND synonym thesauri. $this->getConnection()->insertOnDuplicate( - ThesaurusInterface::EXPANSION_TABLE_NAME, + $this->getTable(ThesaurusInterface::EXPANSION_TABLE_NAME), $expansionTermLinks, array_keys(current($expansionTermLinks)) ); @@ -227,7 +231,7 @@ private function deleteThesaurusRelations(\Magento\Framework\Model\AbstractModel $deleteCondition = [ThesaurusInterface::THESAURUS_ID . " = ?" => $object->getThesaurusId()]; $this->getConnection()->delete( - ThesaurusInterface::EXPANSION_TABLE_NAME, + $this->getTable(ThesaurusInterface::EXPANSION_TABLE_NAME), $deleteCondition ); @@ -246,7 +250,7 @@ private function saveReferenceTerms(\Magento\Framework\Model\AbstractModel $obje { if ($object->getType() === ThesaurusInterface::TYPE_EXPANSION) { $this->getConnection()->insertOnDuplicate( - ThesaurusInterface::REFERENCE_TABLE_NAME, + $this->getTable(ThesaurusInterface::REFERENCE_TABLE_NAME), $referenceTerms, array_keys(current($referenceTerms)) ); diff --git a/src/module-elasticsuite-thesaurus/Model/Thesaurus.php b/src/module-elasticsuite-thesaurus/Model/Thesaurus.php index 3a9e88ce0..f42386190 100644 --- a/src/module-elasticsuite-thesaurus/Model/Thesaurus.php +++ b/src/module-elasticsuite-thesaurus/Model/Thesaurus.php @@ -220,6 +220,28 @@ public function getTermsData() return $this->termsData; } + /** + * Get Thesaurus status + * + * @return bool + */ + public function isActive() + { + return (bool) $this->getData(self::IS_ACTIVE); + } + + /** + * Set Thesaurus status + * + * @param bool $status The thesaurus status + * + * @return ThesaurusInterface + */ + public function setIsActive($status) + { + return $this->setData(self::IS_ACTIVE, (bool) $status); + } + /** * Internal Constructor * diff --git a/src/module-elasticsuite-thesaurus/Setup/UpgradeSchema.php b/src/module-elasticsuite-thesaurus/Setup/UpgradeSchema.php index 9addc6cc9..023127302 100644 --- a/src/module-elasticsuite-thesaurus/Setup/UpgradeSchema.php +++ b/src/module-elasticsuite-thesaurus/Setup/UpgradeSchema.php @@ -40,10 +40,16 @@ public function upgrade(SchemaSetupInterface $setup, ModuleContextInterface $con { $setup->startSetup(); - $this->createThesaurusTable($setup); - $this->createThesaurusStoreTable($setup); - $this->createExpandedTermsTable($setup); - $this->createExpansionReferenceTable($setup); + if (version_compare($context->getVersion(), '0.0.2', '<')) { + $this->createThesaurusTable($setup); + $this->createThesaurusStoreTable($setup); + $this->createExpandedTermsTable($setup); + $this->createExpansionReferenceTable($setup); + } + + if (version_compare($context->getVersion(), '1.0.0', '<')) { + $this->appendIsActiveColumn($setup); + } $setup->endSetup(); } @@ -213,4 +219,23 @@ private function createExpandedTermsTable(SchemaSetupInterface $setup) $setup->getConnection()->createTable($table); } + + /** + * Add an "is_active" column to the Thesaurus table. + * + * @param \Magento\Framework\Setup\SchemaSetupInterface $setup Setup instance + */ + private function appendIsActiveColumn(SchemaSetupInterface $setup) + { + $setup->getConnection()->addColumn( + $setup->getTable(ThesaurusInterface::TABLE_NAME), + ThesaurusInterface::IS_ACTIVE, + [ + 'type' => \Magento\Framework\DB\Ddl\Table::TYPE_SMALLINT, + 'nullable' => false, + 'default' => 1, + 'comment' => 'If the Thesaurus is active', + ] + ); + } } diff --git a/src/module-elasticsuite-thesaurus/composer.json b/src/module-elasticsuite-thesaurus/composer.json index b9fba463c..70d3bd5d8 100644 --- a/src/module-elasticsuite-thesaurus/composer.json +++ b/src/module-elasticsuite-thesaurus/composer.json @@ -20,12 +20,12 @@ ], "require": { "php": "~5.5.0|~5.6.0|~7.0.0", - "magento/framework": "*", - "magento/module-store": "*", - "magento/module-backend": "*", - "magento/module-search": "*", - "magento/module-catalog": "*", - "magento/module-catalog-search": "*", + "magento/framework": ">=100.1.0", + "magento/module-store": ">=100.1.0", + "magento/module-backend": ">=100.1.0", + "magento/module-search": ">=100.1.0", + "magento/module-catalog": ">=100.1.0", + "magento/module-catalog-search": ">=100.1.0", "magento/magento-composer-installer": "*", "elasticsearch/elasticsearch": "2.1.*" }, diff --git a/src/module-elasticsuite-thesaurus/etc/module.xml b/src/module-elasticsuite-thesaurus/etc/module.xml index 46ef89914..778b95ad5 100644 --- a/src/module-elasticsuite-thesaurus/etc/module.xml +++ b/src/module-elasticsuite-thesaurus/etc/module.xml @@ -17,7 +17,7 @@ */ --> - + diff --git a/src/module-elasticsuite-thesaurus/i18n/en_US.csv b/src/module-elasticsuite-thesaurus/i18n/en_US.csv index e23f00b36..c8ae122ac 100644 --- a/src/module-elasticsuite-thesaurus/i18n/en_US.csv +++ b/src/module-elasticsuite-thesaurus/i18n/en_US.csv @@ -44,3 +44,5 @@ Expansions,Expansions Terms,Terms Action,Action Edit,Edit +Active,Active +Inactive,Inactive diff --git a/src/module-elasticsuite-thesaurus/i18n/fr_FR.csv b/src/module-elasticsuite-thesaurus/i18n/fr_FR.csv index 6319ec793..fb219e27b 100644 --- a/src/module-elasticsuite-thesaurus/i18n/fr_FR.csv +++ b/src/module-elasticsuite-thesaurus/i18n/fr_FR.csv @@ -44,3 +44,5 @@ Expansions,Expansions Terms,Termes Action,Action Edit,Editer +Active,Activé +Inactive,Désactivé diff --git a/src/module-elasticsuite-thesaurus/view/adminhtml/layout/smile_elasticsuite_thesaurus_grid_block.xml b/src/module-elasticsuite-thesaurus/view/adminhtml/layout/smile_elasticsuite_thesaurus_grid_block.xml index 97a144704..cca8786c3 100644 --- a/src/module-elasticsuite-thesaurus/view/adminhtml/layout/smile_elasticsuite_thesaurus_grid_block.xml +++ b/src/module-elasticsuite-thesaurus/view/adminhtml/layout/smile_elasticsuite_thesaurus_grid_block.xml @@ -93,6 +93,23 @@ 0 + + + Status + is_active + options + + + 0 + Inactive + + + 1 + Active + + + + action diff --git a/src/module-elasticsuite-tracker/composer.json b/src/module-elasticsuite-tracker/composer.json index 93ec2ded7..fa3cd3284 100644 --- a/src/module-elasticsuite-tracker/composer.json +++ b/src/module-elasticsuite-tracker/composer.json @@ -19,7 +19,7 @@ ], "require": { "php": "~5.5.0|~5.6.0|~7.0.0", - "magento/framework": "*", + "magento/framework": ">=100.1.0", "magento/magento-composer-installer": "*" }, "version": "2.1.0", diff --git a/src/module-elasticsuite-virtual-category/Block/Adminhtml/Catalog/Category/Edit/Tab/Merchandising.php b/src/module-elasticsuite-virtual-category/Block/Adminhtml/Catalog/Category/Edit/Tab/Merchandising.php deleted file mode 100644 index 9994f93fa..000000000 --- a/src/module-elasticsuite-virtual-category/Block/Adminhtml/Catalog/Category/Edit/Tab/Merchandising.php +++ /dev/null @@ -1,279 +0,0 @@ - - * @copyright 2016 Smile - * @license Open Software License ("OSL") v. 3.0 - */ -namespace Smile\ElasticsuiteVirtualCategory\Block\Adminhtml\Catalog\Category\Edit\Tab; - -use Magento\Catalog\Model\Category; -use Magento\Backend\Block\Template; -use Magento\Config\Model\Config\Source\Yesno; - -/** - * Category edit merchandising tab form implementation. - * - * @category Smile - * @package Smile\ElasticsuiteVirtualCategory - * @author Aurelien FOUCRET - */ -class Merchandising extends \Magento\Catalog\Block\Adminhtml\Form -{ - /** - * @var integer - */ - const DEFAULT_PREVIEW_SIZE = 20; - - /** - * @var Category|null - */ - private $category; - - /** - * @var \Magento\Config\Model\Config\Source\Yesno - */ - private $booleanSource; - - /** - * @var \Magento\CatalogRule\Model\RuleFactory - */ - private $ruleFactory; - - /** - * @var integer - */ - private $previewSize; - - /** - * @var \Smile\ElasticsuiteVirtualCategory\Model\ResourceModel\Category\Product\Position - */ - private $productPositionResource; - - /** - * Constructor. - * - * @param \Magento\Backend\Block\Template\Context $context Template context. - * @param \Magento\Framework\Registry $registry Registry (used to read current category) - * @param \Magento\Framework\Data\FormFactory $formFactory Form factory. - * @param \Magento\Config\Model\Config\Source\Yesno $booleanSource Data source for boolean fields. - * @param \Smile\ElasticsuiteCatalogRule\Model\RuleFactory $ruleFactory Catalog product rule factory. - * @param \Smile\ElasticsuiteVirtualCategory\Model\ResourceModel\Category\Product\Position $productPositionResource Product position loading resource. - * @param integer $previewSize Preview size. - * @param array $data Additional data. - */ - public function __construct( - \Magento\Backend\Block\Template\Context $context, - \Magento\Framework\Registry $registry, - \Magento\Framework\Data\FormFactory $formFactory, - \Magento\Config\Model\Config\Source\Yesno $booleanSource, - \Smile\ElasticsuiteCatalogRule\Model\RuleFactory $ruleFactory, - \Smile\ElasticsuiteVirtualCategory\Model\ResourceModel\Category\Product\Position $productPositionResource, - $previewSize = self::DEFAULT_PREVIEW_SIZE, - array $data = [] - ) { - parent::__construct($context, $registry, $formFactory, $data); - - $this->booleanSource = $booleanSource; - $this->ruleFactory = $ruleFactory; - $this->productPositionResource = $productPositionResource; - $this->previewSize = $previewSize; - } - - /** - * Return currently edited category - * - * @return \Magento\Catalog\Model\Category - */ - public function getCategory() - { - if (!$this->category) { - $this->category = $this->_coreRegistry->registry('category'); - } - - return $this->category; - } - - /** - * @SuppressWarnings(PHPMD.CamelCaseMethodName) - * {@inheritDoc} - */ - protected function _prepareLayout() - { - - /** @var \Magento\Framework\Data\Form $form */ - $form = $this->_formFactory->create(); - $form->setDataObject($this->getCategory()); - - $this->addCategoryMode($form) - ->addVirtualCategorySettings($form) - ->addProductSorter($form) - ->addDependenceManager(); - - $form->addValues($this->getCategory()->getData()); - $form->setFieldNameSuffix('general'); - - $this->addFieldRenderers($form); - - $this->setForm($form); - - return parent::_prepareLayout(); - } - - /** - * Append the category mode selector. - * - * @param \Magento\Framework\Data\Form $form Current form. - * - * @return $this - */ - private function addCategoryMode(\Magento\Framework\Data\Form $form) - { - $fieldset = $form->addFieldset('merchandising_category_mode_fieldset', ['legend' => __('Category mode')]); - - $booleanSelectValues = $this->booleanSource->toOptionArray(); - $categoryModeFieldOptions = ['name' => 'is_virtual_category', 'label' => __('Virtual category'), 'values' => $booleanSelectValues]; - $fieldset->addField('is_virtual_category', 'select', $categoryModeFieldOptions); - - return $this; - } - - /** - * Append settings related to a virtual category (category root and rule applied). - * - * @param \Magento\Framework\Data\Form $form Current form. - * - * @return $this - */ - private function addVirtualCategorySettings(\Magento\Framework\Data\Form $form) - { - $fieldset = $form->addFieldset('merchandising_virtual_settings_fieldset', ['legend' => __('Virtual category settings')]); - - // This field is added to manage fieldset dependence to the "is_virtual_category" field. - // @see self::addDependenceManager for more additional information. - $fieldset->addField('virtual_rule_fieldset_visibility_switcher', 'hidden', ['name' => 'virtual_rule_fieldset_visibility_switcher']); - - // Append the virtual rule conditions field. - $fieldset->addField('virtual_rule', 'text', ['name' => 'virtual_rule', 'label' => __('Virtual rule')]); - - // Create the virtual category root selector field. - $categoryChooserFieldOptions = ['name' => 'virtual_category_root', 'label' => __('Virtual category root')]; - $fieldset->addField('virtual_category_root', 'label', $categoryChooserFieldOptions); - - return $this; - } - - /** - * - * @param \Magento\Framework\Data\Form $form Current form. - * - * @return $this - */ - private function addProductSorter(\Magento\Framework\Data\Form $form) - { - $fieldset = $form->addFieldset('merchandising_product_sort_fieldset', ['legend' => __('Preview and sorting')]); - - $fieldset->addField('sorted_products', 'text', ['name' => 'sorted_products']); - - return $this; - } - - /** - * Append renderers to the form. - * - * Note : This is called AFTER calling $form->addValues since the category chooser field renderer is not a - * real renderer and is not applied when the form is rendered but at build time => we need the values are set. - * - * @param \Magento\Framework\Data\Form $form Current form. - * - * @return $this - */ - private function addFieldRenderers(\Magento\Framework\Data\Form $form) - { - // Append the virtual conditions rule renderer. - $virtualRuleField = $form->getElement('virtual_rule'); - $virtualRuleRenderer = $this->getLayout()->createBlock('Smile\ElasticsuiteCatalogRule\Block\Product\Conditions'); - $virtualRuleField->setRenderer($virtualRuleRenderer); - - // Append the virtual category root chooser. - $categoryChooserField = $form->getElement('virtual_category_root'); - $categoryChooserRenderer = $this->getLayout()->createBlock('Magento\Catalog\Block\Adminhtml\Category\Widget\Chooser'); - $categoryChooserRenderer->setFieldsetId($form->getElement('merchandising_virtual_settings_fieldset')->getId()) - ->setConfig(['buttons' => ['open' => __('Select category ...')]]); - $categoryChooserRenderer->prepareElementHtml($categoryChooserField); - - $productSortField = $form->getElement('sorted_products'); - $productSortField->setLoadUrl($this->getPreviewUrl()) - ->setFormId('category_edit_form') - ->setRefreshElements([ - 'category_products', - $form->getElement('is_virtual_category')->getName(), - $form->getElement('virtual_rule')->getName(), - $form->getElement('virtual_category_root')->getName(), - ]) - ->setSavedPositions($this->getProductSavedPositions()) - ->setPageSize($this->previewSize); - - $productSortRenderer = $this->getLayout()->createBlock('Smile\ElasticsuiteCatalog\Block\Adminhtml\Catalog\Product\Form\Renderer\Sort'); - $productSortField->setRenderer($productSortRenderer); - - return $this; - } - - /** - * Apply depedence manegemnt on the form. - * - * Due to the difficulty to manage dependencies between the multiple fieldset we hacked the mechanisms by using an - * arbitary chosen dummy field with a predictable id container to get things working. - * - * @return $this - */ - private function addDependenceManager() - { - $dependenceManagerBlock = $this->getLayout()->createBlock('Magento\Backend\Block\Widget\Form\Element\Dependence'); - - $dependenceManagerBlock->addConfigOptions(['levels_up' => 0]) - ->addFieldMap('is_virtual_category', 'is_virtual_category') - ->addFieldMap('virtual_rule_fieldset_visibility_switcher', 'virtual_rule_fieldset_visibility_switcher') - ->addFieldDependence('virtual_rule_fieldset_visibility_switcher', 'is_virtual_category', 1); - - $this->setChild('form_after', $dependenceManagerBlock); - - return $this; - } - - /** - * Return the product list preview URL. - * - * @return string - */ - private function getPreviewUrl() - { - $storeId = $this->getCategory()->getStoreId(); - - if ($storeId === 0) { - $storeId = current(array_filter($this->getCategory()->getStoreIds())); - } - - $urlParams = ['ajax' => true, 'store' => $storeId]; - - return $this->getUrl('virtualcategory/category_virtual/preview', $urlParams); - } - - /** - * Load product saved positions for the current category. - * - * @return array - */ - private function getProductSavedPositions() - { - return $this->productPositionResource->getProductPositionsByCategory($this->getCategory()); - } -} diff --git a/src/module-elasticsuite-virtual-category/Block/Adminhtml/Catalog/Category/VirtualRule.php b/src/module-elasticsuite-virtual-category/Block/Adminhtml/Catalog/Category/VirtualRule.php new file mode 100644 index 000000000..186004aac --- /dev/null +++ b/src/module-elasticsuite-virtual-category/Block/Adminhtml/Catalog/Category/VirtualRule.php @@ -0,0 +1,97 @@ + + * @copyright 2016 Smile + * @license Open Software License ("OSL") v. 3.0 + */ + +namespace Smile\ElasticsuiteVirtualCategory\Block\Adminhtml\Catalog\Category; + +/** + * Create the virtual rule edit field in the category edit form. + * + * @category Smile + * @package Smile\ElasticsuiteVirtualCategory + * @author Aurelien FOUCRET + */ +class VirtualRule extends \Magento\Backend\Block\AbstractBlock +{ + /** + * @var \Magento\Framework\Data\FormFactory + */ + private $formFactory; + + /** + * @var \Magento\Framework\Registry + */ + private $registry; + + /** + * Constructor. + * + * @param \Magento\Backend\Block\Context $context Block context. + * @param \Magento\Framework\Data\FormFactory $formFactory Form factory. + * @param \Magento\Framework\Registry $registry Registry. + * @param array $data Additional data. + */ + public function __construct( + \Magento\Backend\Block\Context $context, + \Magento\Framework\Data\FormFactory $formFactory, + \Magento\Framework\Registry $registry, + array $data = [] + ) { + $this->formFactory = $formFactory; + $this->registry = $registry; + parent::__construct($context, $data); + } + + /** + * @SuppressWarnings(PHPMD.CamelCaseMethodName) + * {@inheritDoc} + */ + protected function _toHtml() + { + return $this->escapeJsQuote($this->getForm()->toHtml()); + } + + /** + * Returns the currently edited category. + * + * @return \Magento\Catalog\Model\Category + */ + private function getCategory() + { + return $this->registry->registry('category'); + } + + /** + * Create the form containing the virtual rule field. + * + * @return \Magento\Framework\Data\Form + */ + private function getForm() + { + $form = $this->formFactory->create(); + $form->setHtmlId('virtual_rule'); + + $virtualRuleField = $form->addField( + 'virtual_rule', + 'text', + ['name' => 'virtual_rule', 'label' => __('Virtual rule'), 'container_id' => 'virtual_rule'] + ); + + $virtualRuleField->setValue($this->getCategory()->getVirtualRule()); + $virtualRuleRenderer = $this->getLayout()->createBlock('Smile\ElasticsuiteCatalogRule\Block\Product\Conditions'); + $virtualRuleField->setRenderer($virtualRuleRenderer); + + return $form; + } +} diff --git a/src/module-elasticsuite-virtual-category/Controller/Adminhtml/Category/Virtual/Preview.php b/src/module-elasticsuite-virtual-category/Controller/Adminhtml/Category/Virtual/Preview.php index e6ecee106..33478422b 100644 --- a/src/module-elasticsuite-virtual-category/Controller/Adminhtml/Category/Virtual/Preview.php +++ b/src/module-elasticsuite-virtual-category/Controller/Adminhtml/Category/Virtual/Preview.php @@ -38,27 +38,27 @@ class Preview extends Action private $jsonHelper; /** - * @var \Magento\Catalog\Api\CategoryRepositoryInterface + * @var \Magento\Catalog\Model\CategoryFactory */ - private $categoryRepository; + private $categoryFactory; /** * Constructor. * * @param \Magento\Backend\App\Action\Context $context Controller context. * @param \Smile\ElasticsuiteVirtualCategory\Model\PreviewFactory $previewModelFactory Preview model factory. - * @param \Magento\Catalog\Api\CategoryRepositoryInterface $categoryRepository Category repository. + * @param \Magento\Catalog\Model\CategoryFactory $categoryFactory Category factory. * @param \Magento\Framework\Json\Helper\Data $jsonHelper JSON Helper. */ public function __construct( \Magento\Backend\App\Action\Context $context, \Smile\ElasticsuiteVirtualCategory\Model\PreviewFactory $previewModelFactory, - \Magento\Catalog\Api\CategoryRepositoryInterface $categoryRepository, + \Magento\Catalog\Model\CategoryFactory $categoryFactory, \Magento\Framework\Json\Helper\Data $jsonHelper ) { parent::__construct($context); - $this->categoryRepository = $categoryRepository; + $this->categoryFactory = $categoryFactory; $this->previewModelFactory = $previewModelFactory; $this->jsonHelper = $jsonHelper; } @@ -103,26 +103,84 @@ private function getPreviewObject() */ private function getCategory() { - $storeId = $this->getRequest()->getParam('store'); - $category = $this->categoryRepository->get($this->getRequest()->getParam('id'), $storeId); + $category = $this->loadCategory(); - $categoryProductIds = $this->jsonHelper->jsonDecode($this->getRequest()->getParam('category_products')); - $category->setProductIds(array_keys($categoryProductIds)); + $this->addVirtualCategoryData($category) + ->addSelectedProducts($category) + ->setSortedProducts($category); - $categoryPostData = $this->getRequest()->getParam('general', []); + return $category; + } + + /** + * Load current category using the request params. + * + * @return CategoryInterface + */ + private function loadCategory() + { + $category = $this->categoryFactory->create(); + $storeId = $this->getRequest()->getParam('store'); + $categoryId = $this->getRequest()->getParam('entity_id'); + + $category->setStoreId($storeId)->load($categoryId); - $isVirtualCategory = isset($categoryPostData['is_virtual_category']) ? (bool) $categoryPostData['is_virtual_category'] : false; + return $category; + } + + /** + * Append virtual rule params to the category. + * + * @param CategoryInterface $category Category. + * + * @return $this + */ + private function addVirtualCategoryData(CategoryInterface $category) + { + $isVirtualCategory = (bool) $this->getRequest()->getParam('is_virtual_category'); + $category->setIsVirtualCategory($isVirtualCategory); if ($isVirtualCategory) { - $category->setIsVirtualCategory($isVirtualCategory); - $category->getVirtualRule()->loadPost($categoryPostData['virtual_rule']); - $category->setVirtualCategoryRoot($categoryPostData['virtual_category_root']); + $category->getVirtualRule()->loadPost($this->getRequest()->getParam('virtual_rule', [])); + $category->setVirtualCategoryRoot($this->getRequest()->getParam('virtual_category_root', null)); } + return $this; + } + + /** + * Add user selected products. + * + * @param CategoryInterface $category Category. + * + * @return $this + */ + private function addSelectedProducts(CategoryInterface $category) + { + $selectedProducts = $this->getRequest()->getParam('selected_products', []); + + $addedProducts = isset($selectedProducts['added_products']) ? $selectedProducts['added_products'] : []; + $category->setAddedProductIds($addedProducts); + + $deletedProducts = isset($selectedProducts['deleted_products']) ? $selectedProducts['deleted_products'] : []; + $category->setDeletedProductIds($deletedProducts); + + return $this; + } + + /** + * Append products sorted by the user to the category. + * + * @param CategoryInterface $category Category. + * + * @return $this + */ + private function setSortedProducts(CategoryInterface $category) + { $productPositions = $this->getRequest()->getParam('product_position', []); $category->setSortedProductIds(array_keys($productPositions)); - return $category; + return $this; } /** diff --git a/src/module-elasticsuite-virtual-category/Model/Category/Attribute/Backend/VirtualRule.php b/src/module-elasticsuite-virtual-category/Model/Category/Attribute/Backend/VirtualRule.php index 2b3645d47..55ac59367 100644 --- a/src/module-elasticsuite-virtual-category/Model/Category/Attribute/Backend/VirtualRule.php +++ b/src/module-elasticsuite-virtual-category/Model/Category/Attribute/Backend/VirtualRule.php @@ -68,16 +68,21 @@ public function afterLoad($object) $attributeCode = $this->getAttributeCode(); $attributeData = $object->getData($attributeCode); - $rule = $this->ruleFactory->create(); - $rule->setStoreId($object->getStoreId()); + if (!is_object($attributeData)) { + $rule = $this->ruleFactory->create(); + $rule->setStoreId($object->getStoreId()); - if ($attributeData !== null && is_string($attributeData)) { - $attributeData = unserialize($attributeData); - } + if ($attributeData !== null && is_string($attributeData)) { + $attributeData = unserialize($attributeData); + } - $rule->getConditions()->loadArray($attributeData); + if ($attributeData !== null && is_array($attributeData)) { + $rule->getConditions()->loadArray($attributeData); + } + + $object->setData($attributeCode, $rule); + } - $object->setData($attributeCode, $rule); return $this; } diff --git a/src/module-elasticsuite-virtual-category/Model/Preview.php b/src/module-elasticsuite-virtual-category/Model/Preview.php index 4be96f97f..2320a5482 100644 --- a/src/module-elasticsuite-virtual-category/Model/Preview.php +++ b/src/module-elasticsuite-virtual-category/Model/Preview.php @@ -106,7 +106,7 @@ private function getAutomaticSortProductCollection() $productCollection ->setStoreId($this->category->getStoreId()) - ->addQueryFilter($this->getFilterQuery()) + ->addQueryFilter($this->getQueryFilter()) ->addAttributeToSelect(['name', 'small_image']); return $productCollection; @@ -122,11 +122,12 @@ private function getManualSortProductCollection() $productIds = $this->getSortedProductIds(); $productCollection = $this->getAutomaticSortProductCollection(); - $productCollection->setPageSize(count($productIds)); $idFilter = $this->queryFactory->create(QueryInterface::TYPE_TERMS, ['values' => $productIds, 'field' => 'entity_id']); $productCollection->addQueryFilter($idFilter); + $productCollection->setPageSize(count($productIds)); + return $productCollection; } @@ -166,28 +167,43 @@ private function loadItems($products = []) * * @return QueryInterface */ - private function getFilterQuery() + private function getQueryFilter() { - $queryClauses = []; + $queryParams = []; + $this->category->setIsActive(true); - if ($this->category->getIsVirtualCategory()) { - $queryClauses['must'][] = $this->category->getVirtualRule()->getCategorySearchQuery($this->category); - } else { - if (empty($this->category->getProductIds())) { - $this->category->setProductIds([0]); - } + if ($this->category->getIsVirtualCategory() || $this->category->getId()) { + $queryParams['must'][] = $this->category->getVirtualRule()->getCategorySearchQuery($this->category); + } elseif (!$this->category->getId()) { + $queryParams['must'][] = $this->getEntityIdFilterQuery([0]); + } + + if ((bool) $this->category->getIsVirtualCategory() === false) { + $addedProductIds = $this->category->getAddedProductIds(); + $deletedProductIds = $this->category->getDeletedProductIds(); - $queryClauses['should'][] = $this->queryFactory->create( - QueryInterface::TYPE_TERMS, - ['values' => $this->category->getProductIds(), 'field' => 'entity_id'] - ); + if ($addedProductIds && !empty($addedProductIds)) { + $queryParams = ['should' => $queryParams['must']]; + $queryParams['should'][] = $this->getEntityIdFilterQuery($addedProductIds); + } - $childrenQueries = $this->category->getVirtualRule()->getSearchQueriesByChildren($this->category); - foreach ($childrenQueries as $childrenQuery) { - $queryClauses['should'][] = $childrenQuery; + if ($deletedProductIds && !empty($deletedProductIds)) { + $queryParams['mustNot'][] = $this->getEntityIdFilterQuery($deletedProductIds); } } - return $this->queryFactory->create(QueryInterface::TYPE_BOOL, $queryClauses); + return $this->queryFactory->create(QueryInterface::TYPE_BOOL, $queryParams); + } + + /** + * Create a product id filter query. + * + * @param array $ids Id to be filtered. + * + * @return QueryInterface + */ + private function getEntityIdFilterQuery($ids) + { + return $this->queryFactory->create(QueryInterface::TYPE_TERMS, ['field' => 'entity_id', 'values' => $ids]); } } diff --git a/src/module-elasticsuite-virtual-category/Model/ResourceModel/Product/Indexer/Fulltext/Datasource/CategoryData.php b/src/module-elasticsuite-virtual-category/Model/ResourceModel/Product/Indexer/Fulltext/Datasource/CategoryData.php index c3febddd8..c893303f3 100644 --- a/src/module-elasticsuite-virtual-category/Model/ResourceModel/Product/Indexer/Fulltext/Datasource/CategoryData.php +++ b/src/module-elasticsuite-virtual-category/Model/ResourceModel/Product/Indexer/Fulltext/Datasource/CategoryData.php @@ -26,11 +26,6 @@ */ class CategoryData extends \Smile\ElasticsuiteCatalog\Model\ResourceModel\Product\Indexer\Fulltext\Datasource\CategoryData { - /** - * @var array - */ - private $virtualCategoriesIds; - /** * {@inheritDoc} */ @@ -69,7 +64,8 @@ private function getBaseSelect($productIds, $storeId) 'category_id' => 'cpi.category_id', 'product_id' => 'cpi.product_id', 'is_parent' => 'cpi.is_parent', - 'position' => 'p.position', + 'is_virtual' => new \Zend_Db_Expr(0), + 'position' => 'p.position', ]); return $select; @@ -84,53 +80,19 @@ private function getBaseSelect($productIds, $storeId) */ private function getVirtualSelect($productIds) { - $virtualCategoriesIds = $this->getVirtualCategoriesIds(); - $select = $this->getConnection()->select() ->from(['cpi' => $this->getTable(ProductPositionResourceModel::TABLE_NAME)], []) - ->where('cpi.category_id IN (?)', $virtualCategoriesIds) ->where('cpi.product_id IN(?)', $productIds) ->columns( [ 'category_id' => 'cpi.category_id', 'product_id' => 'cpi.product_id', 'is_parent' => new \Zend_Db_Expr('0'), + 'is_virtual' => new \Zend_Db_Expr('1'), 'position' => 'cpi.position', ] ); return $select; } - - /** - * List of the ids of the virtual categories of the site. - * - * @return array - */ - private function getVirtualCategoriesIds() - { - if ($this->virtualCategoriesIds === null) { - $isVirtualAttribute = $this->getIsVirtualCategoryAttribute(); - - $select = $this->getConnection()->select(); - $select->from($isVirtualAttribute->getBackendTable(), ['entity_id']) - ->where('attribute_id = ?', (int) $isVirtualAttribute->getAttributeId()) - ->where('value = ?', true) - ->group('entity_id'); - - $this->virtualCategoriesIds = $this->getConnection()->fetchCol($select); - } - - return $this->virtualCategoriesIds; - } - - /** - * Retrieve the 'is_virtual_category' attribute model. - * - * @return \Magento\Eav\Model\Entity\Attribute\AbstractAttribute - */ - private function getIsVirtualCategoryAttribute() - { - return $this->getEavConfig()->getAttribute(\Magento\Catalog\Model\Category::ENTITY, 'is_virtual_category'); - } } diff --git a/src/module-elasticsuite-virtual-category/Model/Rule.php b/src/module-elasticsuite-virtual-category/Model/Rule.php index 39b57204a..91ffb4582 100644 --- a/src/module-elasticsuite-virtual-category/Model/Rule.php +++ b/src/module-elasticsuite-virtual-category/Model/Rule.php @@ -110,11 +110,10 @@ public function getCategorySearchQuery($category, $excludedCategories = []) $queryParams = []; if ((bool) $category->getIsVirtualCategory() && $category->getIsActive()) { - $parentCategory = $this->getVirtualRootCategory($category); $excludedCategories[] = $category->getId(); - $queryParams['must'][] = $this->getVirtualCategoryQuery($category, $excludedCategories); + $parentCategory = $this->getVirtualRootCategory($category); if ($parentCategory && $parentCategory->getId()) { $queryParams['must'][] = $this->getCategorySearchQuery($parentCategory, $excludedCategories); } @@ -172,7 +171,7 @@ private function getVirtualRootCategory(CategoryInterface $category) $rootCategory = $this->categoryFactory->create()->setStoreId($storeId); if ($category->getVirtualCategoryRoot() !== null && !empty($category->getVirtualCategoryRoot())) { - $rootCategoryId = explode('/', $category->getVirtualCategoryRoot())[1]; + $rootCategoryId = $category->getVirtualCategoryRoot(); $rootCategory->load($rootCategoryId); } diff --git a/src/module-elasticsuite-virtual-category/Model/Rule/Condition/Product/CategoryNestedFilter.php b/src/module-elasticsuite-virtual-category/Model/Rule/Condition/Product/CategoryNestedFilter.php new file mode 100644 index 000000000..7c842c328 --- /dev/null +++ b/src/module-elasticsuite-virtual-category/Model/Rule/Condition/Product/CategoryNestedFilter.php @@ -0,0 +1,62 @@ + + * @copyright 2016 Smile + * @license Open Software License ("OSL") v. 3.0 + */ +namespace Smile\ElasticsuiteVirtualCategory\Model\Rule\Condition\Product; + +use Smile\ElasticsuiteCore\Search\Request\QueryInterface; +use Smile\ElasticsuiteCore\Search\Request\Query\QueryFactory; + +/** + * Category product nested filter. + * + * @category Smile + * @package Smile\ElasticsuiteVirtualCategory + * @author Aurelien FOUCRET + */ +class CategoryNestedFilter implements \Smile\ElasticsuiteCatalogRule\Model\Rule\Condition\Product\NestedFilterInterface +{ + /** + * @var string + */ + const FILTER_FIELD = 'category.is_virtual'; + + /** + * @var QueryFactory + */ + private $queryFactory; + + /** + * Constructor. + * + * @param QueryFactory $queryFactory Query factory. + */ + public function __construct(QueryFactory $queryFactory) + { + $this->queryFactory = $queryFactory; + } + + /** + * {@inheritDoc} + */ + public function getFilter() + { + $filterParams = ['field' => self::FILTER_FIELD, 'value' => true]; + $filterQuery = $this->queryFactory->create( + QueryInterface::TYPE_NOT, + ['query' => $this->queryFactory->create(QueryInterface::TYPE_TERM, $filterParams)] + ); + + return $filterQuery; + } +} diff --git a/src/module-elasticsuite-virtual-category/Observer/CategoryProductFormObserver.php b/src/module-elasticsuite-virtual-category/Observer/CategoryProductFormObserver.php deleted file mode 100644 index e665317a3..000000000 --- a/src/module-elasticsuite-virtual-category/Observer/CategoryProductFormObserver.php +++ /dev/null @@ -1,64 +0,0 @@ - - * @copyright 2016 Smile - * @license Open Software License ("OSL") v. 3.0 - */ -namespace Smile\ElasticsuiteVirtualCategory\Observer; - -use Magento\Catalog\Block\Adminhtml\Category\Tabs; -use Magento\Framework\Event\ObserverInterface; -use Magento\Catalog\Api\Data\CategoryInterface; - -/** - * Handles additional tab for merchandising in the catagory edit page. - * - * @category Smile - * @package Smile\ElasticsuiteVirtualCategory - * @author Aurelien FOUCRET - */ -class CategoryProductFormObserver implements ObserverInterface -{ - /** - * {@inheritDoc} - */ - public function execute(\Magento\Framework\Event\Observer $observer) - { - /** - * @var Tabs $tabs - */ - $tabs = $observer->getEvent()->getTabs(); - - $virtualCategoryFormBlock = $tabs->getLayout()->createBlock( - 'Smile\ElasticsuiteVirtualCategory\Block\Adminhtml\Catalog\Category\Edit\Tab\Merchandising', - 'category.merchandising.form' - ); - - if ($this->canDisplayTab($virtualCategoryFormBlock->getCategory())) { - $tabs->addTab( - 'category.merchandising', - ['label' => __('Merchandising'), 'content' => $virtualCategoryFormBlock->toHtml()] - ); - } - } - - /** - * Indicates if the merchandising tab can be displayed on the current category. - * - * @param CategoryInterface $category Current category. - * - * @return boolean - */ - private function canDisplayTab(CategoryInterface $category) - { - return $category->getId() !== null && $category->getLevel() > 1; - } -} diff --git a/src/module-elasticsuite-virtual-category/Plugin/Catalog/Category/DataProviderPlugin.php b/src/module-elasticsuite-virtual-category/Plugin/Catalog/Category/DataProviderPlugin.php new file mode 100644 index 000000000..30825e449 --- /dev/null +++ b/src/module-elasticsuite-virtual-category/Plugin/Catalog/Category/DataProviderPlugin.php @@ -0,0 +1,126 @@ + + * @copyright 2016 Smile + * @license Open Software License ("OSL") v. 3.0 + */ + +namespace Smile\ElasticsuiteVirtualCategory\Plugin\Catalog\Category; + +use Magento\Catalog\Model\Category\DataProvider as CategoryDataProvider; +use \Smile\ElasticsuiteVirtualCategory\Model\ResourceModel\Category\Product\Position as ProductPositionResource; +use Magento\Catalog\Model\Category; + +/** + * Extenstion of the category form UI data provider. + * + * @category Smile + * @package Smile\ElasticsuiteVirtualCategory + * @author Aurelien FOUCRET + */ +class DataProviderPlugin +{ + /** + * + * @var \Smile\ElasticsuiteVirtualCategory\Model\ResourceModel\Category\Product\Position + */ + private $productPositionResource; + + /** + * @var \Magento\Backend\Model\UrlInterface + */ + private $urlBuilder; + + /** + * @var \Magento\Framework\Locale\FormatInterface + */ + private $localeFormat; + + /** + * Constructor. + * + * @param ProductPositionResource $productPositionResource Product position resource model. + * @param \Magento\Framework\Locale\FormatInterface $localeFormat Locale formater. + * @param \Magento\Backend\Model\UrlInterface $urlBuilder Admin URL Builder. + */ + public function __construct( + ProductPositionResource $productPositionResource, + \Magento\Framework\Locale\FormatInterface $localeFormat, + \Magento\Backend\Model\UrlInterface $urlBuilder + ) { + $this->productPositionResource = $productPositionResource; + $this->localeFormat = $localeFormat; + $this->urlBuilder = $urlBuilder; + } + + /** + * Append virtual rule and sorting product data. + * + * @param CategoryDataProvider $dataProvider Data provider. + * @param \Closure $proceed Original method. + * + * @return array + */ + public function aroundGetData(CategoryDataProvider $dataProvider, \Closure $proceed) + { + $data = $proceed(); + + $currentCategory = $dataProvider->getCurrentCategory(); + + if ($currentCategory->getId() === null || $currentCategory->getLevel() < 2) { + $data[$currentCategory->getId()]['use_default']['is_virtual_category'] = true; + } + + $data[$currentCategory->getId()]['sorted_products'] = $this->getProductSavedPositions($currentCategory); + $data[$currentCategory->getId()]['product_sorter_load_url'] = $this->getProductSorterLoadUrl($currentCategory); + $data[$currentCategory->getId()]['price_format'] = $this->localeFormat->getPriceFormat(); + + return $data; + } + + /** + * Retrieve the category product sorter load URL. + * + * @param Category $category Category. + * + * @return string + */ + private function getProductSorterLoadUrl(Category $category) + { + $storeId = $category->getStoreId(); + + if ($storeId === 0) { + $storeId = current(array_filter($category->getStoreIds())); + } + + $urlParams = ['ajax' => true, 'store' => $storeId]; + + return $this->urlBuilder->getUrl('virtualcategory/category_virtual/preview', $urlParams); + } + + /** + * Load product saved positions for the current category. + * + * @param Category $category Category. + * + * @return array + */ + private function getProductSavedPositions(Category $category) + { + $productPositions = []; + + if ($category->getId()) { + $productPositions = $this->productPositionResource->getProductPositionsByCategory($category); + } + + return json_encode($productPositions, JSON_FORCE_OBJECT); + } +} diff --git a/src/module-elasticsuite-virtual-category/Plugin/Catalog/Category/SaveProductsPositions.php b/src/module-elasticsuite-virtual-category/Plugin/Catalog/Category/SaveProductsPositions.php index 49dc9bc18..bb09727c6 100644 --- a/src/module-elasticsuite-virtual-category/Plugin/Catalog/Category/SaveProductsPositions.php +++ b/src/module-elasticsuite-virtual-category/Plugin/Catalog/Category/SaveProductsPositions.php @@ -75,6 +75,10 @@ public function aroundSave( if ($category->getId() && $category->getSortedProducts()) { $this->unserializeProductPositions($category); + if ($category->getIsVirtualCategory()) { + $category->setPostedProducts([]); + } + $categoryResource->addCommitCallback( function () use ($category) { $affectedProductIds = $this->getAffectedProductIds($category); diff --git a/src/module-elasticsuite-virtual-category/Setup/InstallData.php b/src/module-elasticsuite-virtual-category/Setup/InstallData.php index dab0f8d08..61c0729ea 100644 --- a/src/module-elasticsuite-virtual-category/Setup/InstallData.php +++ b/src/module-elasticsuite-virtual-category/Setup/InstallData.php @@ -82,7 +82,7 @@ public function install(ModuleDataSetupInterface $setup, ModuleContextInterface Category::ENTITY, 'virtual_category_root', [ - 'type' => 'varchar', + 'type' => 'int', 'label' => 'Virtual category root', 'input' => null, 'global' => \Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface::SCOPE_GLOBAL, diff --git a/src/module-elasticsuite-virtual-category/Setup/UpgradeData.php b/src/module-elasticsuite-virtual-category/Setup/UpgradeData.php new file mode 100644 index 000000000..61f754517 --- /dev/null +++ b/src/module-elasticsuite-virtual-category/Setup/UpgradeData.php @@ -0,0 +1,114 @@ + + * @copyright 2016 Smile + * @license Open Software License ("OSL") v. 3.0 + */ +namespace Smile\ElasticsuiteVirtualCategory\Setup; + +use Magento\Catalog\Model\Category; +use Magento\Eav\Setup\EavSetupFactory; +use Magento\Framework\Setup\UpgradeDataInterface; +use Magento\Framework\Setup\ModuleContextInterface; +use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Framework\DB\Adapter\AdapterInterface; + +/** + * Catalog data upgrade. + * + * @category Smile + * @package Smile\ElasticsuiteVirtualCategory + * @author Aurelien FOUCRET + */ +class UpgradeData implements UpgradeDataInterface +{ + /** + * EAV setup factory + * + * @var EavSetupFactory + */ + private $eavSetupFactory; + + /** + * Class Constructor + * + * @param EavSetupFactory $eavSetupFactory Eav setup factory. + */ + public function __construct(EavSetupFactory $eavSetupFactory) + { + $this->eavSetupFactory = $eavSetupFactory; + } + + /** + * Upgrade the module data. + * + * @param ModuleDataSetupInterface $setup The setup interface + * @param ModuleContextInterface $context The module Context + * + * @return void + */ + public function upgrade(ModuleDataSetupInterface $setup, ModuleContextInterface $context) + { + $setup->startSetup(); + + if (version_compare($context->getVersion(), '1.1.0', '<')) { + $this->updateVirtualCategoryRootTypeToInt($setup); + } + + $setup->endSetup(); + } + + /** + * Migration from 1.0.0 to 1.1.0 : + * - Updating the attribute virtual_category_root from type varchar to type int + * - Updating the value of the attribute from 'category/13' to '13. + * + * @param ModuleDataSetupInterface $setup Setup. + * + * @return $this + */ + private function updateVirtualCategoryRootTypeToInt(ModuleDataSetupInterface $setup) + { + /** + * @var \Magento\Eav\Setup\EavSetup $eavSetup + */ + $eavSetup = $this->eavSetupFactory->create(['setup' => $setup]); + + // Fix the attribute type. + $eavSetup->updateAttribute(Category::ENTITY, 'virtual_category_root', 'backend_type', 'int'); + + // Retrieve information about the attribute and storage config. + $virtualRootAttributeId = $eavSetup->getAttribute(Category::ENTITY, 'virtual_category_root', 'attribute_id'); + + $originalTable = $setup->getTable('catalog_category_entity_varchar'); + $targetTable = $setup->getTable('catalog_category_entity_int'); + + $baseFields = array_slice(array_keys($setup->getConnection()->describeTable($originalTable)), 1, -1); + + // Select old value. + $valueSelect = $setup->getConnection()->select(); + $valueSelect->from($setup->getTable('catalog_category_entity_varchar'), $baseFields) + ->where('attribute_id = ?', $virtualRootAttributeId) + ->columns(['value' => new \Zend_Db_Expr('REPLACE(value, "category/", "")')]); + + // Insert old values into the new table. + $query = $setup->getConnection()->insertFromSelect( + $valueSelect, + $targetTable, + array_merge($baseFields, ['value']), + AdapterInterface::INSERT_IGNORE + ); + $setup->getConnection()->query($query); + + // Delete old value. + $setup->getConnection()->delete($originalTable, "attribute_id = {$virtualRootAttributeId}"); + + return $this; + } +} diff --git a/src/module-elasticsuite-virtual-category/composer.json b/src/module-elasticsuite-virtual-category/composer.json index 139fcc5db..32ed33632 100644 --- a/src/module-elasticsuite-virtual-category/composer.json +++ b/src/module-elasticsuite-virtual-category/composer.json @@ -20,7 +20,7 @@ ], "require": { "php": "~5.5.0|~5.6.0|~7.0.0", - "magento/framework": "*", + "magento/framework": ">=100.1.0", "smile/module-elasticsuite-catalog-rule": "self.version" }, "version": "2.1.0", diff --git a/src/module-elasticsuite-virtual-category/etc/adminhtml/events.xml b/src/module-elasticsuite-virtual-category/etc/adminhtml/events.xml deleted file mode 100644 index 442fad001..000000000 --- a/src/module-elasticsuite-virtual-category/etc/adminhtml/events.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - diff --git a/src/module-elasticsuite-virtual-category/etc/config.xml b/src/module-elasticsuite-virtual-category/etc/config.xml new file mode 100644 index 000000000..a12d8b6fd --- /dev/null +++ b/src/module-elasticsuite-virtual-category/etc/config.xml @@ -0,0 +1,26 @@ + + + + + + + 1 + + + + diff --git a/src/module-elasticsuite-virtual-category/etc/di.xml b/src/module-elasticsuite-virtual-category/etc/di.xml index 81741542d..82389b0c2 100644 --- a/src/module-elasticsuite-virtual-category/etc/di.xml +++ b/src/module-elasticsuite-virtual-category/etc/di.xml @@ -35,7 +35,15 @@ Smile\ElasticsuiteVirtualCategory\Model\Rule\Condition\Product - + + + + + + Smile\ElasticsuiteVirtualCategory\Model\Rule\Condition\Product\CategoryNestedFilter + + + @@ -56,4 +64,9 @@ + + + + diff --git a/src/module-elasticsuite-virtual-category/etc/elasticsuite_indices.xml b/src/module-elasticsuite-virtual-category/etc/elasticsuite_indices.xml new file mode 100644 index 000000000..90a723719 --- /dev/null +++ b/src/module-elasticsuite-virtual-category/etc/elasticsuite_indices.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + diff --git a/src/module-elasticsuite-virtual-category/etc/module.xml b/src/module-elasticsuite-virtual-category/etc/module.xml index e22a1f841..7baa3e54d 100644 --- a/src/module-elasticsuite-virtual-category/etc/module.xml +++ b/src/module-elasticsuite-virtual-category/etc/module.xml @@ -17,7 +17,7 @@ */ --> - + diff --git a/src/module-elasticsuite-virtual-category/view/adminhtml/ui_component/category_form.xml b/src/module-elasticsuite-virtual-category/view/adminhtml/ui_component/category_form.xml new file mode 100644 index 000000000..6666ecb55 --- /dev/null +++ b/src/module-elasticsuite-virtual-category/view/adminhtml/ui_component/category_form.xml @@ -0,0 +1,160 @@ + + + + +
+ +
+ + + true + + + + + + + 10 + boolean + checkbox + category + toggle + Virtual Category + + 1 + 0 + + + false + + 0 + + !${ $.provider }:data.use_default.is_virtual_category + + + + + + + + + 20 + + + + + + Smile_ElasticsuiteVirtualCategory/js/component/catalog/category/form/assigned_products + selected_products + + !category_form.category_form.assign_products.is_virtual_category:checked + + + + + + + + + + 20 + + + + + + Magento\Catalog\Ui\Component\Product\Form\Categories\Options + + Virtual Category Root + field + select + Magento_Catalog/js/components/new-category + ui/grid/filters/elements/ui-select + virtual_category_root + true + false + true + false + 1 + 10 + false + + false + + + category_form.category_form.assign_products.is_virtual_category:checked + + + setParsed + + + + + + + Smile\ElasticsuiteVirtualCategory\Block\Adminhtml\Catalog\Category\VirtualRule + + + 20 + Smile_ElasticsuiteCatalogRule/js/component/catalog/product/form/rule + virtual_rule + + category_form.category_form.assign_products.is_virtual_category:checked + + + + + + + + + + + 30 + + + + + + 10 + text + text + category + Smile_ElasticsuiteCatalog/js/form/element/product-sorter + Products List Preview and Sorting + sorted_products + 20 + + is_virtual_category + virtual_rule + virtual_category_root + selected_products.added_products + selected_products.deleted_products + + + ${ $.provider }:data.product_sorter_load_url + ${ $.provider }:data.price_format + + + + + +
+
diff --git a/src/module-elasticsuite-virtual-category/view/adminhtml/web/css/source/_module.less b/src/module-elasticsuite-virtual-category/view/adminhtml/web/css/source/_module.less index 47749ed22..33fe2cebf 100644 --- a/src/module-elasticsuite-virtual-category/view/adminhtml/web/css/source/_module.less +++ b/src/module-elasticsuite-virtual-category/view/adminhtml/web/css/source/_module.less @@ -13,73 +13,6 @@ // */ // Display fieldsets legend into catalog category merchandising tab. -#category_info_tabs_category\.merchandising_content { - .fieldset { - padding: 10px 0 15px; - .legend { - display: block; - } - } -} - -// Hide the position edit column into catalog category products tab -#catalog_category_products { - - .data-grid td:nth-last-child(2) { - border-right-style: solid; - } - - .col-position { - display: none; - } -} - -#merchandising_virtual_settings_fieldset { - - label.label { - width: 100%; - float: none; - text-align: left; - padding: 0px 0px 0 10px; - margin: 0px; - background: #41362f; - color: #FFFFFF; - } - - div.control { - width: 100%; - padding: 0 10px; - } - - .field-virtual_rule { - width: 61%; - float: left; - border: 1px solid #DADADA; - margin: 0 2% 0 0; - } - - .field-virtual_category_root { - float: left; - width: 25%; - border: 1px solid #DADADA; - border-right: none; - - div.control { - label { - padding: 10px; - font-weight: bold; - } - } - } - - .field-chooservirtual_category_root { - float: left; - width: 12%; - border: 1px solid #DADADA; - border-left: none; +.catalog-category-edit { - button { - margin: 6px 0 0; - } - } } diff --git a/src/module-elasticsuite-virtual-category/view/adminhtml/web/js/component/catalog/category/form/assigned_products.js b/src/module-elasticsuite-virtual-category/view/adminhtml/web/js/component/catalog/category/form/assigned_products.js new file mode 100644 index 000000000..437aa40e0 --- /dev/null +++ b/src/module-elasticsuite-virtual-category/view/adminhtml/web/js/component/catalog/category/form/assigned_products.js @@ -0,0 +1,77 @@ +/** + * DISCLAIMER + * + * Do not edit or add to this file if you wish to upgrade Smile Elastic Suite to newer + * versions in the future. + * + * + * @category Smile + * @package Smile\ElasticsuiteVirtualCategory + * @author Aurelien FOUCRET + * @copyright 2016 Smile + * @license Open Software License ("OSL") v. 3.0 + */ + +define([ + 'Magento_Ui/js/form/components/html', + 'underscore', + 'MutationObserver' +], function (Component, _) { + 'use strict'; + + return Component.extend({ + defaults: { + formField: "in_category_products", + links: { + addedProducts: '${ $.provider }:${ $.dataScope }.added_products', + deletedProducts: '${ $.provider }:${ $.dataScope }.deleted_products' + } + }, + initialize: function () { + this._super(); + this.initAssignedProductsListener(); + }, + + initObservable: function () { + this._super(); + this.addedProducts = {}; + this.deletedProducts = {}; + this.observe('addedProducts'); + this.observe('deletedProducts'); + + return this; + }, + + initAssignedProductsListener: function () { + var observer = new MutationObserver(function () { + var selectedProductsField = document.getElementById(this.formField); + if (selectedProductsField) { + observer.disconnect(); + observer = new MutationObserver(this.onProductIdsUpdated.bind(this)); + observerConfig = {attributes: true, attributeFilter: ['value']}; + observer.observe(selectedProductsField, observerConfig); + } + }.bind(this)); + + var observerConfig = {childList: true, subtree: true}; + observer.observe(document, observerConfig); + }, + + onProductIdsUpdated: function (mutations) { + while (mutations.length > 0) { + var currentMutation = mutations.shift(); + var productIds = Object.keys(JSON.parse(currentMutation.target.value)); + this.updateProductIds(productIds); + } + }, + + updateProductIds: function (productIds) { + if (this.initialProductIds === undefined) { + this.initialProductIds = productIds; + } else { + this.addedProducts(_.difference(productIds, this.initialProductIds)); + this.deletedProducts(_.difference(this.initialProductIds, productIds)); + } + } + }) +});