From 4a2210ffab6be1d5e3bcbad16f54edb2419b293d Mon Sep 17 00:00:00 2001 From: Richard BAYET Date: Fri, 21 Dec 2018 17:48:37 +0100 Subject: [PATCH] Store level merchandiser --- .../i18n/de_DE.csv | 5 +- .../i18n/en_US.csv | 5 +- .../i18n/fr_FR.csv | 3 + .../ui_component/search_term_preview.xml | 1 + .../adminhtml/web/css/source/_module.less | 18 +- .../web/js/form/element/product-sorter.js | 83 ++++++++- .../template/form/element/product-sorter.html | 87 +++++---- .../Category/Product/Position.php | 23 ++- .../Fulltext/Datasource/CategoryData.php | 166 +++++++++++++++++- .../Catalog/Category/DataProviderPlugin.php | 14 ++ .../Setup/UpgradeData.php | 4 + .../Setup/UpgradeSchema.php | 4 + .../Setup/VirtualCategorySetup.php | 149 ++++++++++++++++ .../etc/module.xml | 2 +- .../i18n/de_DE.csv | 2 + .../i18n/en_US.csv | 2 + .../i18n/fr_FR.csv | 2 + .../adminhtml/ui_component/category_form.xml | 41 ++++- .../adminhtml/web/css/source/_module.less | 44 +++++ .../web/template/form/element/container.html | 5 + 20 files changed, 606 insertions(+), 54 deletions(-) create mode 100644 src/module-elasticsuite-virtual-category/view/adminhtml/web/template/form/element/container.html diff --git a/src/module-elasticsuite-catalog/i18n/de_DE.csv b/src/module-elasticsuite-catalog/i18n/de_DE.csv index 9f4a55661..621b0a173 100755 --- a/src/module-elasticsuite-catalog/i18n/de_DE.csv +++ b/src/module-elasticsuite-catalog/i18n/de_DE.csv @@ -77,5 +77,8 @@ Pinned,Angeheftet Products,Produkte Categories,Kategorien Attributes,Attribute +"Refine search","Suche einschränken" "Clear search","Suche abbrechen" -"Your search returned no results.","Ihre Suche ergab keine Treffer." \ No newline at end of file +"Your search returned no results.","Ihre Suche ergab keine Treffer." +"Clear product positions","Produktpositionen löschen" +"Clear all products positions and blacklist status ? All products will be reset to be visible and in 'Automatic Sort'.","Alle Produktpositionen und Blacklist-Status löschen ? Alle Produkte werden auf 'Sichtbar' und 'Automatische Sortierung' zurückgesetzt." diff --git a/src/module-elasticsuite-catalog/i18n/en_US.csv b/src/module-elasticsuite-catalog/i18n/en_US.csv index 602d2de8b..974168b8b 100755 --- a/src/module-elasticsuite-catalog/i18n/en_US.csv +++ b/src/module-elasticsuite-catalog/i18n/en_US.csv @@ -77,5 +77,8 @@ Pinned,Pinned Products,Products Categories,Categories Attributes,Attributes +"Refine search","Refine search" "Clear search","Clear search" -"Your search returned no results.","Your search returned no results." \ No newline at end of file +"Your search returned no results.","Your search returned no results." +"Clear product positions","Clear product positions" +"Clear all products positions and blacklist status ? All products will be reset to be visible and in 'Automatic Sort'.","Clear all products positions and blacklist status ? All products will be reset to be visible and in 'Automatic Sort'." diff --git a/src/module-elasticsuite-catalog/i18n/fr_FR.csv b/src/module-elasticsuite-catalog/i18n/fr_FR.csv index ca6c8d7cd..bc43ef1c0 100755 --- a/src/module-elasticsuite-catalog/i18n/fr_FR.csv +++ b/src/module-elasticsuite-catalog/i18n/fr_FR.csv @@ -77,5 +77,8 @@ No,Non Products,Produits Categories,Catégories Attributes,Attributs +"Refine search","Affiner votre recherche" "Clear search","Annuler la recherche" "Your search returned no results.","Votre recherche n'a donné aucun résultat." +"Clear product positions","Réinitialiser les positions" +"Clear all products positions and blacklist status ? All products will be reset to be visible and in 'Automatic Sort'.","Réinitialiser les positions et le masquage des produits ? Tous les produits seront rétablis en 'Tri Automatique' et visibles." diff --git a/src/module-elasticsuite-catalog/view/adminhtml/ui_component/search_term_preview.xml b/src/module-elasticsuite-catalog/view/adminhtml/ui_component/search_term_preview.xml index dbdb09d1d..1e1b60ae9 100644 --- a/src/module-elasticsuite-catalog/view/adminhtml/ui_component/search_term_preview.xml +++ b/src/module-elasticsuite-catalog/view/adminhtml/ui_component/search_term_preview.xml @@ -84,6 +84,7 @@ false + ${ $.provider }:data.product_sorter_load_url 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 d4a3c76e2..dae3b03b7 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 @@ -26,6 +26,9 @@ a.action-reset { margin: 0 1rem; } + .admin__control-text { + width: auto; + } } .bottom-links { @@ -50,9 +53,22 @@ margin: 20px 100px 20px 100px; } + .product-list-container { + position: relative; + + .admin__data-grid-loading-mask { + position: absolute; + } + } + + button.reset { + float: right; + } + .product-list { - margin: 20px 0 0; + margin: 0; + display: inline-block; li { box-sizing: content-box; 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 index f7b1bd779..4728d23c2 100644 --- 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 @@ -16,9 +16,10 @@ define([ 'Magento_Ui/js/form/element/abstract', 'jquery', 'Smile_ElasticsuiteCatalog/js/form/element/product-sorter/item', + 'Magento_Ui/js/modal/confirm', 'MutationObserver', 'ko' -], function (Component, $, Product, MutationObserver, ko) { +], function (Component, $, Product, confirm, MutationObserver, ko) { 'use strict'; return Component.extend({ @@ -30,24 +31,37 @@ define([ maxRefreshInterval: 1000, imports: { formData: "${ $.provider }:data", - blacklistedProducts: "${ $.provider }:data.blacklisted_products" + blacklistedProducts: "${ $.provider }:data.blacklisted_products", + defaultBlacklistedProducts: "${ $.provider }:data.default.blacklisted_products", + defaultSortedProducts: "${ $.provider }:data.default.sorted_products" }, links: { - blacklistedProducts: "${ $.provider }.data.blacklisted_products" + blacklistedProducts: "${ $.provider }.data.blacklisted_products", + defaultBlacklistedProducts: "${ $.provider }:data.default.blacklisted_products", + defaultSortedProducts: "${ $.provider }:data.default.sorted_products" }, messages : { emptyText : $.mage.__('Your product selection is empty.'), automaticSort : $.mage.__('Automatic Sort'), manualSort : $.mage.__('Manual Sort'), + showMore : $.mage.__('Show more'), search : $.mage.__('Search'), + searchLabel : $.mage.__('Refine search'), clearSearch : $.mage.__('Clear search'), noResultsText : $.mage.__('Your search returned no results.'), - showMore : $.mage.__('Show more') + previewOnlyModeText : $.mage.__('Preview Only Mode'), + resetAllText : $.mage.__('Clear product positions'), + resetAllQuestionText : $.mage.__('Clear all products positions and blacklist status ?') }, forceLoading : false, allowBlacklist : false, allowSearch: false, + previewOnlyMode : false, blacklistedProducts: [], + defaultSortedProducts: "{}", + defaultBlacklistedProducts: [], + storeSortedProducts: "{}", + storeBlacklistedProducts: [], modules: { provider: '${ $.provider }' } @@ -65,8 +79,10 @@ define([ this.currentSize = this.pageSize; this.enabled = this.loadUrl != null; this.search = ko.observable(""); + this.previewOnlyMode = (this.scopeSwitcher != null) && (parseInt(this.scopeSwitcher, 10) == 1) && (this.formData.store_id != 0); + this.initialSwitchCopy = this.previewOnlyMode; - this.observe(['products', 'countTotalProducts', 'currentSize', 'editPositions', 'loading', 'showSpinner', 'blacklistedProducts']); + this.observe(['products', 'countTotalProducts', 'currentSize', 'editPositions', 'loading', 'showSpinner', 'blacklistedProducts', 'previewOnlyMode']); this.editPositions.subscribe(function () { this.value(JSON.stringify(this.editPositions())); }.bind(this)); @@ -100,6 +116,57 @@ define([ } }, + switchScope: function(useDefaultPositions) { + if (parseInt(useDefaultPositions, 10) == 1) { + // Backup current store level positions and blacklist. + this.storeSortedProducts = JSON.stringify(this.editPositions()); + this.storeBlacklistedProducts = this.blacklistedProducts().slice(0); + // Switch positions and blacklist. + this.editPositions = JSON.parse(this.defaultSortedProducts); + this.blacklistedProducts(this.defaultBlacklistedProducts.slice(0)); + } else { + if (this.initialSwitchCopy) { + // Copy current (default) positions and blacklist to store level. + this.storeSortedProducts = JSON.stringify(this.editPositions()); + this.storeBlacklistedProducts = this.blacklistedProducts().slice(0); + this.initialSwitchCopy = false; + } + // Restore store level positions and blacklist. + this.editPositions = JSON.parse(this.storeSortedProducts); + this.blacklistedProducts(this.storeBlacklistedProducts.slice(0)); + } + // Recreate required observers/subscriptions. + this.observe(['editPositions']); + this.editPositions.subscribe(function () { this.value(JSON.stringify(this.editPositions())); }.bind(this)); + + this.previewOnlyMode(!this.previewOnlyMode()); + + this.refreshProductList(); + }, + + resetAllProducts: function() { + confirm({ + content: this.messages.resetAllQuestionText, + actions: { + /** + * Confirm action. + */ + confirm: function () { + this.editPositions = JSON.parse("{}"); + this.blacklistedProducts([]); + + // Recreate required observers/subscriptions. + this.observe(['editPositions']); + this.editPositions.subscribe(function () { this.value(JSON.stringify(this.editPositions())); }.bind(this)); + + this.refreshProductList(); + }.bind(this) + } + }); + + return false; + }, + refreshProductList: function () { if (this.refreshRateLimiter !== undefined) { clearTimeout(); @@ -243,6 +310,9 @@ define([ }, toggleSortType: function (product) { + if (this.previewOnlyMode()) { + return; + } var products = this.products(); var editPositions = this.editPositions(); @@ -275,6 +345,9 @@ define([ }, toggleBlackListed: function(product) { + if (this.previewOnlyMode()) { + return; + } var state = !product.isBlacklisted(); product.setIsBlacklisted(state); 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 index a46afc069..d25c04d25 100644 --- 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 @@ -10,6 +10,23 @@ +
+
+
+
+
+ +
+ +
+ +
+
+
@@ -17,44 +34,44 @@
- -
-
    -
  • -
    -
    - -
    - -
    - -
    -

    -

    -

    -

    -
    - -
    - - -
    -
  • -
+
+
+ +
    +
  • +
    +
    + +
    + +
    +
    +

    +

    +

    +

    +
    + +
    + + +
    +
  • +
+
+ diff --git a/src/module-elasticsuite-virtual-category/Model/ResourceModel/Category/Product/Position.php b/src/module-elasticsuite-virtual-category/Model/ResourceModel/Category/Product/Position.php index a492aad73..756d30991 100644 --- a/src/module-elasticsuite-virtual-category/Model/ResourceModel/Category/Product/Position.php +++ b/src/module-elasticsuite-virtual-category/Model/ResourceModel/Category/Product/Position.php @@ -40,12 +40,17 @@ class Position extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb */ public function getProductPositionsByCategory($category) { + $storeId = 0; if (is_object($category)) { + if (!$category->getUseDefaultPositions()) { + $storeId = $category->getStoreId(); + } $category = $category->getId(); } $select = $this->getBaseSelect() ->where('category_id = ?', (int) $category) + ->where('store_id = ?', (int) $storeId) ->where('position IS NOT NULL') ->columns(['product_id', 'position']); @@ -61,20 +66,25 @@ public function getProductPositionsByCategory($category) */ public function getProductBlacklistByCategory($category) { + $storeId = 0; if (is_object($category)) { + if (!$category->getUseDefaultPositions()) { + $storeId = $category->getStoreId(); + } $category = $category->getId(); } $select = $this->getBaseSelect() ->columns(['product_id']) ->where('category_id = ?', (int) $category) + ->where('store_id = ?', (int) $storeId) ->where('is_blacklisted = ?', (int) true); return $this->getConnection()->fetchCol($select); } /** - * Save the product postions. + * Save the product positions. * * @param CategoryInterface $category Saved category. * @@ -82,11 +92,21 @@ public function getProductBlacklistByCategory($category) */ public function saveProductPositions(CategoryInterface $category) { + // Can be 0 if not on a store view. + $storeId = (int) $category->getStoreId(); + + // If on a store view, and no store override of positions, clean up existing store records. + if ($storeId && $category->getUseDefaultPositions()) { + $category->setSortedProducts([]); + $category->setBlacklistedProducts([]); + } + $newProductPositions = $category->getSortedProducts(); $blacklistedProducts = $category->getBlacklistedProducts() ?? []; $deleteConditions = [ $this->getConnection()->quoteInto('category_id = ?', (int) $category->getId()), + $this->getConnection()->quoteInto('store_id = ?', $storeId), ]; if (!empty($newProductPositions) || !empty($blacklistedProducts)) { @@ -97,6 +117,7 @@ public function saveProductPositions(CategoryInterface $category) $insertData[] = [ 'category_id' => $category->getId(), 'product_id' => $productId, + 'store_id' => $storeId, 'position' => $newProductPositions[$productId] ?? null, 'is_blacklisted' => in_array($productId, $blacklistedProducts), ]; 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 15ddcab6c..aa02f9e1b 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 @@ -16,6 +16,8 @@ namespace Smile\ElasticsuiteVirtualCategory\Model\ResourceModel\Product\Indexer\Fulltext\Datasource; use Smile\ElasticsuiteVirtualCategory\Model\ResourceModel\Category\Product\Position as ProductPositionResourceModel; +use Magento\Catalog\Api\Data\CategoryAttributeInterface; +use Magento\Catalog\Api\Data\CategoryInterface; /** * Category datasource override. Saves product positions set from admin. @@ -26,6 +28,11 @@ */ class CategoryData extends \Smile\ElasticsuiteCatalog\Model\ResourceModel\Product\Indexer\Fulltext\Datasource\CategoryData { + /** + * @var null|CategoryAttributeInterface + */ + private $useDefaultPositionsAttribute = null; + /** * {@inheritDoc} */ @@ -33,8 +40,10 @@ protected function getCategoryProductSelect($productIds, $storeId) { $select = $this->getConnection()->select()->union( [ - $this->getBaseSelect($productIds, $storeId), - $this->getVirtualSelect($productIds, $storeId), + $this->getBaseSelectGlobal($productIds, $storeId), + $this->getBaseSelectStore($productIds, $storeId), + $this->getVirtualSelectGlobal($productIds, $storeId), + $this->getVirtualSelectStore($productIds, $storeId), ] ); @@ -43,23 +52,88 @@ protected function getCategoryProductSelect($productIds, $storeId) /** * Retrieve the standard categories product data (categories ids, positions, ...). + * Product positions returned are those defined globally. * * @param array $productIds Product ids. * @param int $storeId Store id. * * @return \Zend_Db_Select */ - private function getBaseSelect($productIds, $storeId) + private function getBaseSelectGlobal($productIds, $storeId) { + $useDefaultPositionsAttr = $this->getUseDefaultPositionsAttribute(); + $linkField = $this->getEntityMetaData(CategoryInterface::class)->getLinkField(); + + $conditions = [ + "cpi.category_id = use_default_positions.{$linkField}", + "use_default_positions.store_id = " . $storeId, + "use_default_positions.attribute_id = " . (int) $useDefaultPositionsAttr->getAttributeId(), + ]; + $joinCondition = new \Zend_Db_Expr(implode(" AND ", $conditions)); + $select = $this->getConnection()->select() ->from(['cpi' => $this->getTable($this->getCategoryProductIndexTable($storeId))], []) ->joinLeft( ['p' => $this->getTable(ProductPositionResourceModel::TABLE_NAME)], - 'p.product_id = cpi.product_id AND p.category_id = cpi.category_id', + 'p.product_id = cpi.product_id AND p.category_id = cpi.category_id AND p.store_id = 0', + [] + ) + ->joinLeft( + ['use_default_positions' => $useDefaultPositionsAttr->getBackendTable()], + $joinCondition, + [] + ) + ->where('cpi.store_id = ?', $storeId) + ->where('cpi.product_id IN(?)', $productIds) + ->where(new \Zend_Db_Expr("COALESCE(use_default_positions.value, 1) = 1")) + ->columns([ + 'category_id' => 'cpi.category_id', + 'product_id' => 'cpi.product_id', + 'is_parent' => 'cpi.is_parent', + 'is_virtual' => new \Zend_Db_Expr('"false"'), + 'position' => 'p.position', + 'is_blacklisted' => 'p.is_blacklisted', + ]); + + return $select; + } + + /** + * Retrieve the standard categories product data (categories ids, positions, ...). + * Product positions returned are those defined at the store level. + * + * @param array $productIds Product ids. + * @param int $storeId Store id. + * + * @return \Zend_Db_Select + */ + private function getBaseSelectStore($productIds, $storeId) + { + $useDefaultPositionsAttr = $this->getUseDefaultPositionsAttribute(); + $linkField = $this->getEntityMetaData(CategoryInterface::class)->getLinkField(); + + $conditions = [ + "cpi.category_id = use_default_positions.{$linkField}", + "use_default_positions.store_id = " . $storeId, + "use_default_positions.attribute_id = " . (int) $useDefaultPositionsAttr->getAttributeId(), + ]; + $joinCondition = new \Zend_Db_Expr(implode(" AND ", $conditions)); + + $select = $this->getConnection()->select() + ->from(['cpi' => $this->getTable($this->getCategoryProductIndexTable($storeId))], []) + ->joinLeft( + ['p' => $this->getTable(ProductPositionResourceModel::TABLE_NAME)], + 'p.product_id = cpi.product_id AND p.category_id = cpi.category_id AND p.store_id = cpi.store_id', + [] + ) + ->joinLeft( + ['use_default_positions' => $useDefaultPositionsAttr->getBackendTable()], + $joinCondition, [] ) ->where('cpi.store_id = ?', $storeId) ->where('cpi.product_id IN(?)', $productIds) + ->where(new \Zend_Db_Expr("COALESCE(use_default_positions.value, 1) = 0")) ->columns([ 'category_id' => 'cpi.category_id', 'product_id' => 'cpi.product_id', @@ -74,14 +148,76 @@ private function getBaseSelect($productIds, $storeId) /** * Retrieve the virtual categories product data (categories ids, positions, ...). + * Product positions returned are those defined globally. + * + * @param array $productIds Product ids. + * @param integer $storeId Store id. + * + * @return \Zend_Db_Select + */ + private function getVirtualSelectGlobal($productIds, $storeId) + { + $useDefaultPositionsAttr = $this->getUseDefaultPositionsAttribute(); + $linkField = $this->getEntityMetaData(CategoryInterface::class)->getLinkField(); + + $conditions = [ + "p.category_id = use_default_positions.{$linkField}", + "use_default_positions.store_id = " . $storeId, + "use_default_positions.attribute_id = " . (int) $useDefaultPositionsAttr->getAttributeId(), + ]; + $joinCondition = new \Zend_Db_Expr(implode(" AND ", $conditions)); + + $select = $this->getConnection()->select() + ->from(['p' => $this->getTable(ProductPositionResourceModel::TABLE_NAME)], []) + ->joinLeft( + ['cpi' => $this->getTable($this->getCategoryProductIndexTable($storeId))], + 'p.product_id = cpi.product_id AND p.category_id = cpi.category_id', + [] + ) + ->joinLeft( + ['use_default_positions' => $useDefaultPositionsAttr->getBackendTable()], + $joinCondition, + [] + ) + ->where('p.product_id IN(?)', $productIds) + ->where('cpi.product_id IS NULL') + ->where(new \Zend_Db_Expr("COALESCE(use_default_positions.value, 1) = 1")) + ->where('p.store_id = 0') + ->columns( + [ + 'category_id' => 'p.category_id', + 'product_id' => 'p.product_id', + 'is_parent' => new \Zend_Db_Expr('0'), + 'is_virtual' => new \Zend_Db_Expr('"true"'), + 'position' => 'p.position', + 'is_blacklisted' => 'p.is_blacklisted', + ] + ); + + return $select; + } + + /** + * Retrieve the virtual categories product data (categories ids, positions, ...). + * Product positions returned are those defined locally. * * @param array $productIds Product ids. * @param integer $storeId Store id. * * @return \Zend_Db_Select */ - private function getVirtualSelect($productIds, $storeId) + private function getVirtualSelectStore($productIds, $storeId) { + $useDefaultPositionsAttr = $this->getUseDefaultPositionsAttribute(); + $linkField = $this->getEntityMetaData(CategoryInterface::class)->getLinkField(); + + $conditions = [ + "p.category_id = use_default_positions.{$linkField}", + "use_default_positions.store_id = " . $storeId, + "use_default_positions.attribute_id = " . (int) $useDefaultPositionsAttr->getAttributeId(), + ]; + $joinCondition = new \Zend_Db_Expr(implode(" AND ", $conditions)); + $select = $this->getConnection()->select() ->from(['p' => $this->getTable(ProductPositionResourceModel::TABLE_NAME)], []) ->joinLeft( @@ -89,8 +225,15 @@ private function getVirtualSelect($productIds, $storeId) 'p.product_id = cpi.product_id AND p.category_id = cpi.category_id', [] ) + ->joinLeft( + ['use_default_positions' => $useDefaultPositionsAttr->getBackendTable()], + $joinCondition, + [] + ) ->where('p.product_id IN(?)', $productIds) ->where('cpi.product_id IS NULL') + ->where(new \Zend_Db_Expr("COALESCE(use_default_positions.value, 1) = 0")) + ->where('p.store_id = ?', $storeId) ->columns( [ 'category_id' => 'p.category_id', @@ -104,4 +247,17 @@ private function getVirtualSelect($productIds, $storeId) return $select; } + + /** + * Returns category attribute "use store positions" + * + * @return \Magento\Eav\Model\Entity\Attribute\AbstractAttribute + */ + private function getUseDefaultPositionsAttribute() + { + $this->useDefaultPositionsAttribute = $this->getEavConfig() + ->getAttribute(\Magento\Catalog\Model\Category::ENTITY, 'use_default_positions'); + + return $this->useDefaultPositionsAttribute; + } } diff --git a/src/module-elasticsuite-virtual-category/Plugin/Catalog/Category/DataProviderPlugin.php b/src/module-elasticsuite-virtual-category/Plugin/Catalog/Category/DataProviderPlugin.php index bda018e62..9e07fd2e3 100644 --- a/src/module-elasticsuite-virtual-category/Plugin/Catalog/Category/DataProviderPlugin.php +++ b/src/module-elasticsuite-virtual-category/Plugin/Catalog/Category/DataProviderPlugin.php @@ -92,6 +92,20 @@ public function aroundGetData(CategoryDataProvider $dataProvider, \Closure $proc $data[$currentCategory->getId()]['virtual_category_root'] = $currentCategory->getPathIds()[1]; } + if (!$currentCategory->getStoreId() || $currentCategory->getId() === null) { + $data[$currentCategory->getId()]['use_default']['use_default_positions'] = true; + } + + // To restore global/"All store views" positions/blacklist. + $data[$currentCategory->getId()]['default']['sorted_products'] = []; + $data[$currentCategory->getId()]['default']['blacklisted_products'] = []; + if ($currentCategory->getStoreId()) { + $globalCategory = clone $currentCategory; + $globalCategory->setUseDefaultPositions(true); + $data[$currentCategory->getId()]['default']['sorted_products'] = $this->getProductSavedPositions($globalCategory); + $data[$currentCategory->getId()]['default']['blacklisted_products'] = $this->getBlacklistedProducts($globalCategory); + } + $data[$currentCategory->getId()]['sorted_products'] = $this->getProductSavedPositions($currentCategory); $data[$currentCategory->getId()]['blacklisted_products'] = $this->getBlacklistedProducts($currentCategory); $data[$currentCategory->getId()]['product_sorter_load_url'] = $this->getProductSorterLoadUrl($currentCategory); diff --git a/src/module-elasticsuite-virtual-category/Setup/UpgradeData.php b/src/module-elasticsuite-virtual-category/Setup/UpgradeData.php index bf1752fa3..b07cf7b2f 100644 --- a/src/module-elasticsuite-virtual-category/Setup/UpgradeData.php +++ b/src/module-elasticsuite-virtual-category/Setup/UpgradeData.php @@ -73,6 +73,10 @@ public function upgrade(ModuleDataSetupInterface $setup, ModuleContextInterface $this->virtualCategorySetup->convertSerializedRulesToJson($this->eavSetupFactory->create(['setup' => $setup])); } + if (version_compare($context->getVersion(), '1.4.0', '<')) { + $this->virtualCategorySetup->addUseDefaultPositionsAttribute($this->eavSetupFactory->create(['setup' => $setup])); + } + $setup->endSetup(); } } diff --git a/src/module-elasticsuite-virtual-category/Setup/UpgradeSchema.php b/src/module-elasticsuite-virtual-category/Setup/UpgradeSchema.php index 1da9bb1a0..240c737b7 100644 --- a/src/module-elasticsuite-virtual-category/Setup/UpgradeSchema.php +++ b/src/module-elasticsuite-virtual-category/Setup/UpgradeSchema.php @@ -53,6 +53,10 @@ public function upgrade(SchemaSetupInterface $setup, ModuleContextInterface $con $this->virtualCategorySetup->setNullablePositionColumn($setup); } + if (version_compare($context->getVersion(), '1.4.0', '<')) { + $this->virtualCategorySetup->addStoreIdColumnToPositionTable($setup); + } + $setup->endSetup(); } } diff --git a/src/module-elasticsuite-virtual-category/Setup/VirtualCategorySetup.php b/src/module-elasticsuite-virtual-category/Setup/VirtualCategorySetup.php index 428ca85b7..686a911f6 100644 --- a/src/module-elasticsuite-virtual-category/Setup/VirtualCategorySetup.php +++ b/src/module-elasticsuite-virtual-category/Setup/VirtualCategorySetup.php @@ -12,9 +12,11 @@ */ namespace Smile\ElasticsuiteVirtualCategory\Setup; +use Magento\Catalog\Api\Data\CategoryInterface; use Magento\Catalog\Model\Category; use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\Setup\SchemaSetupInterface; +use Magento\Framework\EntityManager\MetadataPool; /** * Generic Setup class for Virtual Categories @@ -27,6 +29,11 @@ */ class VirtualCategorySetup { + /** + * @var \Magento\Framework\EntityManager\MetadataPool + */ + private $metadataPool; + /** * @var \Magento\Eav\Model\Config $eavConfig */ @@ -55,6 +62,7 @@ class VirtualCategorySetup /** * VirtualCategorySetup constructor. * + * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool Metadata Pool. * @param \Magento\Eav\Model\Config $eavConfig EAV Config. * @param \Magento\Framework\DB\FieldDataConverterFactory $fieldDataConverterFactory Field Data converter factory. * @param \Magento\Framework\DB\Select\QueryModifierFactory $queryModifierFactory Query Modifier Factory. @@ -62,12 +70,14 @@ class VirtualCategorySetup * @param \Magento\Catalog\Model\Indexer\Category\Flat\State $flatCategoryIndexState Category flat index state. */ public function __construct( + \Magento\Framework\EntityManager\MetadataPool $metadataPool, \Magento\Eav\Model\Config $eavConfig, \Magento\Framework\DB\FieldDataConverterFactory $fieldDataConverterFactory, \Magento\Framework\DB\Select\QueryModifierFactory $queryModifierFactory, \Magento\Framework\Indexer\IndexerRegistry $indexerRegistry, \Magento\Catalog\Model\Indexer\Category\Flat\State $flatCategoryIndexState ) { + $this->metadataPool = $metadataPool; $this->eavConfig = $eavConfig; $this->fieldDataConverterFactory = $fieldDataConverterFactory; $this->queryModifierFactory = $queryModifierFactory; @@ -137,6 +147,8 @@ public function createVirtualCategoriesAttributes($eavSetup) $eavSetup->updateAttribute(Category::ENTITY, 'virtual_category_root', 'frontend_input', null); $eavSetup->updateAttribute(Category::ENTITY, 'virtual_rule', 'frontend_input', null); + $this->addUseDefaultPositionsAttribute($eavSetup); + // Mandatory to ensure next installers will have proper EAV Attributes definitions. $this->eavConfig->clear(); } @@ -220,6 +232,13 @@ public function createPositionTable(SchemaSetupInterface $setup) ['unsigned' => true, 'nullable' => false, 'primary' => true, 'default' => '0'], 'Product ID' ) + ->addColumn( + 'store_id', + \Magento\Framework\DB\Ddl\Table::TYPE_SMALLINT, + null, + ['unsigned' => true, 'nullable' => false, 'primary' => true, 'default' => '0'], + 'Store ID' + ) ->addColumn( 'position', \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER, @@ -249,6 +268,13 @@ public function createPositionTable(SchemaSetupInterface $setup) 'entity_id', \Magento\Framework\DB\Ddl\Table::ACTION_CASCADE ) + ->addForeignKey( + $setup->getFkName($tableName, 'store_id', 'store', 'store_id'), + 'store_id', + $setup->getTable('store'), + 'store_id', + \Magento\Framework\DB\Ddl\Table::ACTION_CASCADE + ) ->setComment('Catalog product position for the virtual categories module.'); $setup->getConnection()->createTable($table); @@ -291,6 +317,48 @@ public function setNullablePositionColumn(SchemaSetupInterface $setup) ); } + /** + * Add 'store_id' column to 'smile_virtualcategory_catalog_category_product_position' + * and make it part of the table compound primary key. + * + * @param \Magento\Framework\Setup\SchemaSetupInterface $setup Setup interface + */ + public function addStoreIdColumnToPositionTable(SchemaSetupInterface $setup) + { + $tableName = $setup->getTable('smile_virtualcategory_catalog_category_product_position'); + + $setup->getConnection()->addColumn( + $tableName, + 'store_id', + [ + 'type' => \Magento\Framework\DB\Ddl\Table::TYPE_SMALLINT, + 'unsigned' => true, + 'nullable' => false, + 'default' => 0, + 'comment' => 'Store ID', + 'after' => 'product_id', + ] + ); + + $primaryKeyName = $setup->getConnection()->getPrimaryKeyName($tableName); + // The existing primary key will be dropped. + $setup->getConnection()->addIndex( + $tableName, + $primaryKeyName, + ['category_id', 'product_id', 'store_id'], + AdapterInterface::INDEX_TYPE_PRIMARY + ); + + $setup->getConnection()->addForeignKey( + $setup->getFkName($tableName, 'store_id', $setup->getTable('store'), 'store_id'), + $tableName, + 'store_id', + $setup->getTable('store'), + 'store_id', + \Magento\Framework\DB\Ddl\Table::ACTION_CASCADE + ); + } + /** * Remove the backend model of the 'virtual_rule' attribute. * @@ -345,6 +413,40 @@ public function convertSerializedRulesToJson(\Magento\Eav\Setup\EavSetup $eavSet $this->reindexFlatCategories(); } + /** + * Add the attribute handling the per-store merchandiser + * + * @param \Magento\Eav\Setup\EavSetup $eavSetup EAV Setup + * + * @return void + */ + public function addUseDefaultPositionsAttribute(\Magento\Eav\Setup\EavSetup $eavSetup) + { + $eavSetup->addAttribute( + Category::ENTITY, + 'use_default_positions', + [ + 'type' => 'int', + 'input' => 'select', + 'source' => 'Magento\Eav\Model\Entity\Attribute\Source\Boolean', + 'label' => 'Use default positions', + 'global' => \Magento\Eav\Model\Entity\Attribute\ScopedAttributeInterface::SCOPE_STORE, + 'required' => true, + 'default' => 1, + 'visible' => true, + 'note' => "Use default positions.", + 'sort_order' => 220, + 'group' => 'General Information', + ] + ); + + // Set the attribute value to 1 for all existing categories. + $this->updateCategoryAttributeDefaultValue($eavSetup, Category::ENTITY, 'use_default_positions', 1); + + // Mandatory to ensure next installers will have proper EAV Attributes definitions. + $this->eavConfig->clear(); + } + /** * Process full reindexing of flat categories if enabled and not scheduled. */ @@ -355,4 +457,51 @@ private function reindexFlatCategories() $flatCategoryIndexer->reindexAll(); } } + + /** + * Update attribute value for an entity with a default value. + * All existing values are erased by the new value. + * + * @param \Magento\Eav\Setup\EavSetup $eavSetup EAV module Setup + * @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 updateCategoryAttributeDefaultValue($eavSetup, $entityTypeId, $attributeId, $value, $excludedIds = []) + { + $setup = $eavSetup->getSetup(); + $entityTable = $setup->getTable($eavSetup->getEntityType($entityTypeId, 'entity_table')); + $attributeTable = $eavSetup->getAttributeTable($entityTypeId, $attributeId); + $connection = $setup->getConnection(); + + if (!is_int($attributeId)) { + $attributeId = $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 = $connection->select(); + $entitySelect->from( + $entityTable, + [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 = $connection->insertFromSelect( + $entitySelect, + $attributeTable, + ['attribute_id', $linkField, 'value'], + \Magento\Framework\DB\Adapter\AdapterInterface::INSERT_ON_DUPLICATE + ); + + $connection->query($insertQuery); + } } diff --git a/src/module-elasticsuite-virtual-category/etc/module.xml b/src/module-elasticsuite-virtual-category/etc/module.xml index 091f47e5a..a513706a1 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/i18n/de_DE.csv b/src/module-elasticsuite-virtual-category/i18n/de_DE.csv index dc37a0cbc..e804f781f 100644 --- a/src/module-elasticsuite-virtual-category/i18n/de_DE.csv +++ b/src/module-elasticsuite-virtual-category/i18n/de_DE.csv @@ -4,3 +4,5 @@ "Products List Preview and Sorting","Produktliste Vorschau und Sortierung" "Your product selection is empty for the selected Store View. If you are running a multi-store setup, please check this manual page for more informations.","Für die gewählte Store View ist die Produktauswahl leer. Bei Einsatz eines Multi-Store Setups bitte folgende Anleitung berücksichtigen." "Cannot move the category : '%2' is using '%1' as virtual root category.","Kategorie kann nicht verschoben werden: '%2' verwendet '%1' als virtuelle Root-Kategorie." +"Use default positions","Standardpositionen verwenden" +"In this mode, you can only preview products position and visibility but not change them. If you want to change them globally, switch to the 'All Store Views' Store View. If you want to change them for the currently selected Store View, change 'Use default positions' to 'No'.","In diesem Modus können Sie nur eine Vorschau der Produktposition und der Sichtbarkeit anzeigen, sie jedoch nicht ändern. Wenn Sie sie global ändern möchten, wechseln Sie in die Store View 'All Store Views'. Wenn Sie sie für die aktuell ausgewählte Store View ändern möchten, ändern Sie "Standardpositionen verwenden" in 'Nein'." \ No newline at end of file diff --git a/src/module-elasticsuite-virtual-category/i18n/en_US.csv b/src/module-elasticsuite-virtual-category/i18n/en_US.csv index 026d1a73c..df97fc4ab 100644 --- a/src/module-elasticsuite-virtual-category/i18n/en_US.csv +++ b/src/module-elasticsuite-virtual-category/i18n/en_US.csv @@ -4,3 +4,5 @@ "Products List Preview and Sorting","Products List Preview and Sorting" "Your product selection is empty for the selected Store View. If you are running a multi-store setup, please check this manual page for more informations.","Your product selection is empty for the selected Store View. If you are running a multi-store setup, please check this manual page for more informations." "Cannot move the category : '%2' is using '%1' as virtual root category.","Cannot move the category : '%2' is using '%1' as virtual root category." +"Use default positions","Use default positions" +"In this mode, you can only preview products position and visibility but not change them. If you want to change them globally, switch to the 'All Store Views' Store View. If you want to change them for the currently selected Store View, change 'Use default positions' to 'No'.","In this mode, you can only preview products position and visibility but not change them. If you want to change them globally, switch to the 'All Store Views' Store View. If you want to change them for the currently selected Store View, change 'Use default positions' to 'No'." \ No newline at end of file diff --git a/src/module-elasticsuite-virtual-category/i18n/fr_FR.csv b/src/module-elasticsuite-virtual-category/i18n/fr_FR.csv index 89d7c124a..d0b290703 100644 --- a/src/module-elasticsuite-virtual-category/i18n/fr_FR.csv +++ b/src/module-elasticsuite-virtual-category/i18n/fr_FR.csv @@ -4,3 +4,5 @@ "Products List Preview and Sorting","Prévisualisation et tri" "Your product selection is empty for the selected Store View. If you are running a multi-store setup, please check this manual page for more informations.","Votre sélection de produits est vide pour la vue magasin actuellement sélectionnée. Si votre site comporte plusieurs vues magasin, consultez cette page du manuel pour plus d'informations." "Cannot move the category : '%2' is using '%1' as virtual root category.","Impossible de déplacer la catégorie : '%2' utilise actuellement '%1' comme racine." +"Use default positions","Utiliser les positions par défaut" +"In this mode, you can only preview products position and visibility but not change them. If you want to change them globally, switch to the 'All Store Views' Store View. If you want to change them for the currently selected Store View, change 'Use default positions' to 'No'.","Dans ce mode, vous ne pouvez que prévisualiser la position des produits et leur visibilité. Si vous souhaitez les modifier de façon globale, basculer sur la Vue Magasin 'Toutes les vues magasin'. Si vous souhaitez les modifier pour la Vue Magasin actuellement sélectionnée, basculez 'Utiliser les positions par défaut' à 'Non'." \ No newline at end of file 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 index 8f8d324a1..d92087ae6 100644 --- 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 @@ -125,12 +125,37 @@ - + 30 + Products List Preview and Sorting + + + + + boolean + checkbox + category + Use default positions + toggle + + 1 + 0 + + + false + + 1 + + !${ $.provider }:data.use_default.use_default_positions + + + + + @@ -139,13 +164,15 @@ text category Smile_ElasticsuiteCatalog/js/form/element/product-sorter - Products List Preview and Sorting sorted_products 20 true true manual page for more informations.]]> + + + is_virtual_category @@ -162,9 +189,15 @@ ${ $.provider }:data.product_sorter_load_url ${ $.provider }:data.price_format ${ $.provider }:data.blacklisted_products + ${ $.provider }:data.default.blacklisted_products + ${ $.provider }:data.default.sorted_products + ${ $.provider }:data.use_default_positions - - + + switchScope + + + 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 54bb2c123..b9b24b59d 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 @@ -16,3 +16,47 @@ .catalog-category-edit { } + +.elasticsuite-admin-product-sorter-container { + + margin: 40px 20px; + + > span.title { + font-size: 1.7rem; + font-weight: 600; + letter-spacing: .025em; + display: inline-block; + margin-bottom: 10px; + } + + .elasticsuite-admin-product-sorter { + margin: 0; + + .elasticsuite-admin-product-sorter-empty { + margin: 0 100px; + } + } + + .elasticsuite-admin-product-sorter-preview-mode { + margin-bottom: 10px; + } + + .admin__field { + border: 0; + margin: 0; + padding: 0; + #mix-grid .row(); + + &:after { + clear: both; + content: ''; + display: table; + } + > .admin__field-label { + #mix-grid .column(@field-label-grid__column, @field-grid__columns); + } + > .admin__field-control { + #mix-grid .column(@field-control-grid__column, @field-grid__columns); + } + } +} diff --git a/src/module-elasticsuite-virtual-category/view/adminhtml/web/template/form/element/container.html b/src/module-elasticsuite-virtual-category/view/adminhtml/web/template/form/element/container.html new file mode 100644 index 000000000..d031455c4 --- /dev/null +++ b/src/module-elasticsuite-virtual-category/view/adminhtml/web/template/form/element/container.html @@ -0,0 +1,5 @@ +
+ + + +