diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AdminOpenSalesCheckoutConfigPageActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AdminOpenSalesCheckoutConfigPageActionGroup.xml new file mode 100644 index 0000000000000..4e76e3113bdb8 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AdminOpenSalesCheckoutConfigPageActionGroup.xml @@ -0,0 +1,21 @@ + + + + + + Goes to the Store Configuration > Sales > Checkout configuration page in admin. + + + + + + + + + diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AdminSelectClearShoppingCartConfigurationActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AdminSelectClearShoppingCartConfigurationActionGroup.xml new file mode 100644 index 0000000000000..7d9cc0ca90d4e --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AdminSelectClearShoppingCartConfigurationActionGroup.xml @@ -0,0 +1,23 @@ + + + + + + + Enable/Disable clear shopping cart store configuration using UI. + + + + + + + + + + diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontClearShoppingCartActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontClearShoppingCartActionGroup.xml new file mode 100644 index 0000000000000..2582cba5a6871 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontClearShoppingCartActionGroup.xml @@ -0,0 +1,27 @@ + + + + + + + Clicks the Clear Shopping Cart button on the storefront on the shopping cart page and verifies shopping cart gets emptied. + + + + + + + + + + + + + + diff --git a/app/code/Magento/Checkout/Test/Mftf/Data/ConfigData.xml b/app/code/Magento/Checkout/Test/Mftf/Data/ConfigData.xml index bb47a2fcc3070..9ab8a64c9ab88 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Data/ConfigData.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Data/ConfigData.xml @@ -100,4 +100,17 @@ Display number of items in cart 0 + + + checkout/cart/enable_clear_shopping_cart + Display clear shopping cart button on the cart page + 1 + Yes + + + checkout/cart/enable_clear_shopping_cart + Do not display clear shopping cart button on the cart page + 0 + No + diff --git a/app/code/Magento/Checkout/Test/Mftf/Page/AdminCheckoutConfigPage.xml b/app/code/Magento/Checkout/Test/Mftf/Page/AdminCheckoutConfigPage.xml new file mode 100644 index 0000000000000..21d69a1ad93c7 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Page/AdminCheckoutConfigPage.xml @@ -0,0 +1,12 @@ + + + + +
+ + diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/AdminCheckoutConfigSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/AdminCheckoutConfigSection.xml new file mode 100644 index 0000000000000..72cba8349ec0b --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Section/AdminCheckoutConfigSection.xml @@ -0,0 +1,13 @@ + + + +
+ + +
+
diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml index af9d81249e8ac..84f9a7930d40b 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml @@ -48,6 +48,9 @@ + + + diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/ClearShoppingCartEnableDisableConfigurationTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/ClearShoppingCartEnableDisableConfigurationTest.xml new file mode 100644 index 0000000000000..92a4b9563ab3d --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/ClearShoppingCartEnableDisableConfigurationTest.xml @@ -0,0 +1,86 @@ + + + + + + + + + <description value="Verify that disabling the clear shopping cart store configuration will remove the clear shopping cart configuration button from the storefront's shopping cart page. Verify that enabling the configuration will add the button to the page and that the button functions as expected"/> + <group value="shoppingCart"/> + <severity value="MAJOR"/> + </annotations> + <before> + <!-- Create simple products and category --> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct1"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createProduct2"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct1" stepKey="deleteProduct1"/> + <deleteData createDataKey="createProduct2" stepKey="deleteProduct2"/> + + <!-- Disable clear shopping cart --> + <magentoCLI command="config:set {{DisableClearShoppingCart.path}} {{DisableClearShoppingCart.value}}" stepKey="disableClearShoppingCart"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <!-- Navigate to sales checkout cart configuration --> + <actionGroup ref="AdminOpenSalesCheckoutConfigPageActionGroup" stepKey="openSalesCheckoutCartConfig1"> + <argument name="tabGroupAnchor" value="#checkout_cart-link"/> + </actionGroup> + + <!-- Enable clear shopping cart button --> + <actionGroup ref="AdminSelectClearShoppingCartConfigurationActionGroup" stepKey="enableClearShoppingCartButton"/> + <actionGroup ref="SaveStoreConfigurationActionGroup" stepKey="saveStoreConfiguration1"/> + + <!-- Open product 1 and add to cart --> + <actionGroup ref="StorefrontOpenProductEntityPageActionGroup" stepKey="openProduct1Page1"> + <argument name="product" value="$$createProduct1$$"/> + </actionGroup> + <actionGroup ref="StorefrontAddToTheCartActionGroup" stepKey="product1AddToCart"/> + + <!-- Open product 2 and add to cart --> + <actionGroup ref="StorefrontOpenProductEntityPageActionGroup" stepKey="openProduct2Page"> + <argument name="product" value="$$createProduct2$$"/> + </actionGroup> + <actionGroup ref="StorefrontAddToTheCartActionGroup" stepKey="product2AddToCart"/> + + <!-- Go to shopping cart page --> + <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="openShoppingCartPage1"/> + + <!-- Clear shopping cart --> + <actionGroup ref="StorefrontClearShoppingCartActionGroup" stepKey="clearShoppingCart"/> + <actionGroup ref="AssertMiniCartEmptyActionGroup" stepKey="assertMiniCartEmpty"/> + + <!-- Return to Admin to disable clear shopping cart --> + <actionGroup ref="AdminOpenSalesCheckoutConfigPageActionGroup" stepKey="openSalesCheckoutCartConfig2"/> + <actionGroup ref="AdminSelectClearShoppingCartConfigurationActionGroup" stepKey="disableClearShoppingCartButton"> + <argument name="value" value="{{DisableClearShoppingCart.textValue}}"/> + </actionGroup> + <actionGroup ref="SaveStoreConfigurationActionGroup" stepKey="saveStoreConfiguration2"/> + + <!-- Open product 1 page and add to cart --> + <actionGroup ref="StorefrontOpenProductEntityPageActionGroup" stepKey="openProduct1Page2"> + <argument name="product" value="$$createProduct1$$"/> + </actionGroup> + <actionGroup ref="StorefrontAddToTheCartActionGroup" stepKey="product1AddToCart2"/> + + <!-- Go to shopping cart and assert clear shopping cart button is not rendered in UI --> + <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="openShoppingCartPage2"/> + <dontSeeElementInDOM selector="{{CheckoutCartProductSection.emptyCartButton}}" stepKey="dontSeeElementEmptyCartButton"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/ViewModel/Cart.php b/app/code/Magento/Checkout/ViewModel/Cart.php new file mode 100644 index 0000000000000..f5415079d396e --- /dev/null +++ b/app/code/Magento/Checkout/ViewModel/Cart.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Checkout\ViewModel; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\View\Element\Block\ArgumentInterface; +use Magento\Framework\View\Element\Context; +use Magento\Store\Model\ScopeInterface; + +/** + * Cart form view model. + */ +class Cart implements ArgumentInterface +{ + /** + * Config settings path to enable clear shopping cart button + */ + private const XPATH_CONFIG_ENABLE_CLEAR_SHOPPING_CART = 'checkout/cart/enable_clear_shopping_cart'; + + /** + * @var ScopeConfigInterface + */ + private $_scopeConfig; + + /** + * Constructor + * + * @param Context $context + */ + public function __construct( + Context $context + ) { + $this->_scopeConfig = $context->getScopeConfig(); + } + + /** + * Check if clear shopping cart button is enabled + * + * @return bool + */ + public function isClearShoppingCartEnabled() + { + return (bool)$this->_scopeConfig->getValue( + self::XPATH_CONFIG_ENABLE_CLEAR_SHOPPING_CART, + ScopeInterface::SCOPE_WEBSITE + ); + } +} diff --git a/app/code/Magento/Checkout/etc/adminhtml/system.xml b/app/code/Magento/Checkout/etc/adminhtml/system.xml index 7454c2b6524f3..b56566a043c3e 100644 --- a/app/code/Magento/Checkout/etc/adminhtml/system.xml +++ b/app/code/Magento/Checkout/etc/adminhtml/system.xml @@ -48,6 +48,10 @@ <label>Show Cross-sell Items in the Shopping Cart</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> + <field id="enable_clear_shopping_cart" translate="label" type="select" sortOrder="20" showInDefault="1" showInWebsite="1" canRestore="1"> + <label>Enable Clear Shopping Cart</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + </field> </group> <group id="cart_link" translate="label" sortOrder="3" showInDefault="1" showInWebsite="1"> <label>My Cart Link</label> diff --git a/app/code/Magento/Checkout/etc/config.xml b/app/code/Magento/Checkout/etc/config.xml index c8408f6d902fa..4db5f5bdc01c9 100644 --- a/app/code/Magento/Checkout/etc/config.xml +++ b/app/code/Magento/Checkout/etc/config.xml @@ -19,6 +19,7 @@ <redirect_to_cart>0</redirect_to_cart> <number_items_to_display_pager>20</number_items_to_display_pager> <crosssell_enabled>1</crosssell_enabled> + <enable_clear_shopping_cart>0</enable_clear_shopping_cart> </cart> <cart_link> <use_qty>1</use_qty> diff --git a/app/code/Magento/Checkout/i18n/en_US.csv b/app/code/Magento/Checkout/i18n/en_US.csv index 251985faf6cc4..4a78f8deae841 100644 --- a/app/code/Magento/Checkout/i18n/en_US.csv +++ b/app/code/Magento/Checkout/i18n/en_US.csv @@ -153,6 +153,8 @@ Shipping,Shipping "Maximum Number of Items to Display in Order Summary","Maximum Number of Items to Display in Order Summary" "Quote Lifetime (days)","Quote Lifetime (days)" "After Adding a Product Redirect to Shopping Cart","After Adding a Product Redirect to Shopping Cart" +"Enable Clear Shopping Cart","Enable Clear Shopping Cart" +"Are you sure you want to remove all items from your shopping cart?","Are you sure you want to remove all items from your shopping cart?" "Number of Items to Display Pager","Number of Items to Display Pager" "My Cart Link","My Cart Link" "Display Cart Summary","Display Cart Summary" diff --git a/app/code/Magento/Checkout/view/frontend/layout/checkout_cart_index.xml b/app/code/Magento/Checkout/view/frontend/layout/checkout_cart_index.xml index 81ee1a5e6db4c..b465c68078641 100644 --- a/app/code/Magento/Checkout/view/frontend/layout/checkout_cart_index.xml +++ b/app/code/Magento/Checkout/view/frontend/layout/checkout_cart_index.xml @@ -181,6 +181,9 @@ </block> </container> <block class="Magento\Checkout\Block\Cart\Grid" name="checkout.cart.form" as="cart-items" template="Magento_Checkout::cart/form.phtml" after="cart.summary"> + <arguments> + <argument name="view_model" xsi:type="object">Magento\Checkout\ViewModel\Cart</argument> + </arguments> <block class="Magento\Framework\View\Element\RendererList" name="checkout.cart.item.renderers" as="renderer.list"/> <block class="Magento\Framework\View\Element\Text\ListText" name="checkout.cart.order.actions"/> </block> diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml index 370d70c44d886..59e33a7c855ce 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml @@ -20,7 +20,7 @@ class="form form-cart"> <?= $block->getBlockHtml('formkey') ?> <div class="cart table-wrapper<?= $mergedCells == 2 ? ' detailed' : '' ?>"> - <?php if ($block->getPagerHtml()) :?> + <?php if ($block->getPagerHtml()): ?> <div class="cart-products-toolbar cart-products-toolbar-top toolbar" data-attribute="cart-products-toolbar-top"><?= $block->getPagerHtml() ?> </div> @@ -38,32 +38,34 @@ <th class="col subtotal" scope="col"><span><?= $block->escapeHtml(__('Subtotal')) ?></span></th> </tr> </thead> - <?php foreach ($block->getItems() as $_item) :?> + <?php foreach ($block->getItems() as $_item): ?> <?= $block->getItemHtml($_item) ?> <?php endforeach ?> </table> - <?php if ($block->getPagerHtml()) :?> + <?php if ($block->getPagerHtml()): ?> <div class="cart-products-toolbar cart-products-toolbar-bottom toolbar" data-attribute="cart-products-toolbar-bottom"><?= $block->getPagerHtml() ?> </div> <?php endif ?> </div> <div class="cart main actions"> - <?php if ($block->getContinueShoppingUrl()) :?> + <?php if ($block->getContinueShoppingUrl()): ?> <a class="action continue" href="<?= $block->escapeUrl($block->getContinueShoppingUrl()) ?>" title="<?= $block->escapeHtml(__('Continue Shopping')) ?>"> <span><?= $block->escapeHtml(__('Continue Shopping')) ?></span> </a> <?php endif; ?> - <button type="button" - name="update_cart_action" - data-cart-empty="" - value="empty_cart" - title="<?= $block->escapeHtml(__('Clear Shopping Cart')) ?>" - class="action clear" id="empty_cart_button"> - <span><?= $block->escapeHtml(__('Clear Shopping Cart')) ?></span> - </button> + <?php if ($block->getViewModel()->isClearShoppingCartEnabled()): ?> + <button type="button" + name="update_cart_action" + data-cart-empty="" + value="empty_cart" + title="<?= $block->escapeHtml(__('Clear Shopping Cart')) ?>" + class="action clear" id="empty_cart_button"> + <span><?= $block->escapeHtml(__('Clear Shopping Cart')) ?></span> + </button> + <?php endif ?> <button type="submit" name="update_cart_action" data-cart-item-update="" @@ -77,4 +79,3 @@ </form> <?= $block->getChildHtml('checkout.cart.order.actions') ?> <?= $block->getChildHtml('shopping.cart.table.after') ?> - diff --git a/app/code/Magento/Checkout/view/frontend/web/js/shopping-cart.js b/app/code/Magento/Checkout/view/frontend/web/js/shopping-cart.js index b15599673095f..97dff2f6fd47a 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/shopping-cart.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/shopping-cart.js @@ -5,8 +5,10 @@ define([ 'jquery', - 'jquery-ui-modules/widget' -], function ($) { + 'Magento_Ui/js/modal/confirm', + 'jquery-ui-modules/widget', + 'mage/translate' +], function ($, confirm) { 'use strict'; $.widget('mage.shoppingCart', { @@ -15,13 +17,7 @@ define([ var items, i, reload; $(this.options.emptyCartButton).on('click', $.proxy(function () { - $(this.options.emptyCartButton).attr('name', 'update_cart_action_temp'); - $(this.options.updateCartActionContainer) - .attr('name', 'update_cart_action').attr('value', 'empty_cart'); - - if ($(this.options.emptyCartButton).parents('form').length > 0) { - $(this.options.emptyCartButton).parents('form').submit(); - } + this._confirmClearCart(); }, this)); items = $.find('[data-role="cart-item-qty"]'); @@ -61,6 +57,40 @@ define([ $('div.block.block-minicart').off('dropdowndialogclose'); })); }, this)); + }, + + /** + * Display confirmation modal for clearing the cart + * @private + */ + _confirmClearCart: function () { + var self = this; + + confirm({ + content: $.mage.__('Are you sure you want to remove all items from your shopping cart?'), + actions: { + /** + * Confirmation modal handler to execute clear cart action + */ + confirm: function () { + self.clearCart(); + } + } + }); + }, + + /** + * Prepares the form and submit to clear the cart + * @public + */ + clearCart: function () { + $(this.options.emptyCartButton).attr('name', 'update_cart_action_temp'); + $(this.options.updateCartActionContainer) + .attr('name', 'update_cart_action').attr('value', 'empty_cart'); + + if ($(this.options.emptyCartButton).parents('form').length > 0) { + $(this.options.emptyCartButton).parents('form').submit(); + } } }); diff --git a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_cart.less b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_cart.less index 87990c3e48280..5d9746317af55 100644 --- a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_cart.less +++ b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_cart.less @@ -424,7 +424,14 @@ .cart-container { .form-cart { .actions.main { - text-align: center; + .lib-vendor-prefix-display(); + .lib-vendor-prefix-flex-direction(column); + .lib-vendor-box-align(center); + + .clear, + .continue { + .lib-css(margin, 0 0 @indent__m 0); + } } } } diff --git a/app/design/frontend/Magento/luma/web/css/source/_extends.less b/app/design/frontend/Magento/luma/web/css/source/_extends.less index ce86b690f6252..8ae1776daf239 100644 --- a/app/design/frontend/Magento/luma/web/css/source/_extends.less +++ b/app/design/frontend/Magento/luma/web/css/source/_extends.less @@ -1570,10 +1570,16 @@ margin-bottom: @indent__base; .actions.main { - .continue, - .clear { + .continue { display: none; } + + .clear { + .lib-button-as-link( + @_margin: 0 @indent__base 0 0 + ); + font-weight: @font-weight__regular; + } } } } diff --git a/dev/tests/integration/testsuite/Magento/Checkout/ViewModel/CartTest.php b/dev/tests/integration/testsuite/Magento/Checkout/ViewModel/CartTest.php new file mode 100644 index 0000000000000..7fb57ca0f4090 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/ViewModel/CartTest.php @@ -0,0 +1,125 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Checkout\ViewModel; + +use Magento\Framework\App\Config\MutableScopeConfigInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for clear shopping cart config + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class CartTest extends TestCase +{ + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var Cart + */ + private $cart; + + /** + * @var MutableScopeConfigInterface + */ + private $mutableScopeConfig; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $objectManager = $this->objectManager = Bootstrap::getObjectManager(); + $this->cart = $objectManager->get(Cart::class); + $this->mutableScopeConfig = $objectManager->get(MutableScopeConfigInterface::class); + $this->storeManager = $objectManager->get(StoreManagerInterface::class); + } + + /** + * @magentoAppArea frontend + * @magentoDataFixture Magento/Store/_files/second_website_with_two_stores.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testConfigClearShoppingCartEnabledWithWebsiteScopes() + { + // Assert not active by default + $this->assertFalse($this->cart->isClearShoppingCartEnabled()); + + // Enable Clear Shopping Cart in default website scope + $this->setClearShoppingCartEnabled( + true, + ScopeInterface::SCOPE_WEBSITE + ); + + // Assert now active in default website scope + $this->assertTrue($this->cart->isClearShoppingCartEnabled()); + + $defaultStore = $this->storeManager->getStore(); + $defaultWebsite = $defaultStore->getWebsite(); + $defaultWebsiteCode = $defaultWebsite->getCode(); + + $secondStore = $this->storeManager->getStore('fixture_second_store'); + $secondWebsite = $secondStore->getWebsite(); + $secondWebsiteCode = $secondWebsite->getCode(); + + // Change current store context to that of second website + $this->storeManager->setCurrentStore($secondStore); + + // Assert not active by default in second website + $this->assertFalse($this->cart->isClearShoppingCartEnabled()); + + // Enable Clear Shopping Cart in second website scope + $this->setClearShoppingCartEnabled( + true, + ScopeInterface::SCOPE_WEBSITE, + $secondWebsiteCode + ); + + // Assert now active in second website scope + $this->assertTrue($this->cart->isClearShoppingCartEnabled()); + + // Disable Clear Shopping Cart in default website scope + $this->setClearShoppingCartEnabled( + false, + ScopeInterface::SCOPE_WEBSITE, + $defaultWebsiteCode + ); + + // Assert still active in second website + $this->assertTrue($this->cart->isClearShoppingCartEnabled()); + } + + /** + * Set clear shopping cart enabled. + * + * @param bool $isActive + * @param string $scope + * @param string|null $scopeCode + */ + private function setClearShoppingCartEnabled(bool $isActive, string $scope, $scopeCode = null) + { + $this->mutableScopeConfig->setValue( + 'checkout/cart/enable_clear_shopping_cart', + $isActive ? '1' : '0', + $scope, + $scopeCode + ); + } +}