Magento 2 SmartSearch: Best Smart Search Auto Complete Module

Here we are going to learn how we can make our Magento 2 catalog search to a Magento 2 SmartSearch. Wishusucess Smart Search Autocomplete extension gives a better user experience by giving the most relevant search results.

When customers come to your store and try to search for something then it's our responsility to provide products they want and make a purchase.

So this is one of the most important solutions which increase the sales on the store and makes e-commerce store successful.

When customers find the right product when they search that means your usability is increasing and increasing the store usability is also the most important point in any e-commerce store success.

Our Magento 2 expert developer developed the Wishusucess Smart Search Autocomplete which will be very helpful for any store to keep a good customer experiance.

 

Magento 2 SmartSearch AutoComplete Basic File

When we create module like smartsearch in magento then we  need to create some importnt file. Here all the basic files are:

app/code/Wishusucess/SmartSearch/registration.php

app/code/Wishusucess/SmartSearch/etc/module.xml

app/code/Wishusucess/SmartSearch/etc/di.xml

app/code/Wishusucess/SmartSearch/Model/Autocomplete/SearchDataProvider.php

app/code/Wishusucess/SmartSearch/view/frontend/layout/default.xml

app/code/Wishusucess/SmartSearch/view/frontend/templates/form.mini.phtml

app/code/Wishusucess/SmartSearch/view/frontend/requirejs-config.js

app/code/Wishusucess/SmartSearch/view/frontend/web/form-mini.js

app/code/Wishusucess/SmartSearch/view/frontend/web/css/module.less

 

Wishusucess Magento 2 Smart Search Features

  • This SmartSearch module provides the best search experience.
  • Shows relevant product suggestions.
  • Wishuscess SmartSearch also adds Advanced Search Autocomplete.
  • Gives product thumbnail, price, product label, rating, in-stock status in product search suggestion result.
  • It offers the most relevant sort order of products when we go on the search results page.

Magento 2 SmartSearch Module

 

Best Compatibility With Magento 2.3

We have tested it on Magento 2.3 and we get the best result in this Magento version and but this module is quite compatible with Magento 2.1, 2.2, 2.3, and 2.4.

 

Step 1: Register Your SmartSearch

app/code/Wishusucess/SmartSearch/registration.php

<?php
/*
| Registration file for SmartSearch Module
|
| @author Hemant Singh
| @date 23 november 2015
| http://www.wishusucess.com/
*/

\Magento\Framework\Component\ComponentRegistrar::register(
\Magento\Framework\Component\ComponentRegistrar::MODULE,
'Wishusucess_SmartSearch',
__DIR__
);

 

Step 2: Give The Basic Information About Your Module

app/code/Wishusucess/SmartSearch/etc/module.xml

<?xml version="1.0"?>
<!--
/**
* Copyright © 2015 Magento. All rights reserved.
* See COPYING.txt for license details.
*
* Author: Hemant Singh
* Website: http://www.wishusucess.com/
*/
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
<module name="Wishusucess_SmartSearch" setup_version="2.0.0"/>
</config>

 

Step 3: Constructor Arguments

Now we have to configure our class constructor arguments and for that, we have to create di.xml in the argument node.

app/code/Wishusucess/SmartSearch/etc/di.xml

<?xml version="1.0"?>
<!--
/**
* Copyright © 2015 Magento. All rights reserved.
* See COPYING.txt for license details.
*
* Author: Hemant Singh
* Website URL: http://www.wishusucess.com/
*/
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<preference for="Magento\CatalogSearch\Model\Autocomplete\DataProvider" type="Wishusucess\SmartSearch\Model\Autocomplete\SearchDataProvider" />
</config>

 

Step 4: Data Provider of Magento 2 SmartSearch

This Data Provider class will allow us to define the things once and run them many times with any kinds of inputs at the same time or different times, for checking multiple use cases.

app/code/Wishusucess/SmartSearch/Model/Autocomplete/SearchDataProvider.php

<?php
/*
|--------------------------------------------------------------------------
| Autocomplete SearchDataProvider
|--------------------------------------------------------------------------
|
| Autocomplete
|
| @author Hemant Singh
| http://www.wishusucess.com/
*/
namespace Wishusucess\SmartSearch\Model\Autocomplete;

use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Catalog\Helper\Image;
use Magento\Framework\Api\FilterBuilder;
use Magento\Framework\Api\Search\FilterGroupBuilder;
use Magento\Framework\Api\Search\SearchCriteriaFactory as FullTextSearchCriteriaFactory;
use Magento\Framework\Api\Search\SearchInterface as FullTextSearchApi;
use Magento\Framework\Api\SearchCriteriaBuilder;
use Magento\Framework\Pricing\PriceCurrencyInterface;
use Magento\Search\Model\Autocomplete\DataProviderInterface;
use Magento\Search\Model\Autocomplete\ItemFactory;
use Magento\Search\Model\QueryFactory;
use Magento\Store\Model\StoreManagerInterface;

/**
* Full text search implementation of autocomplete.
*
* @package Sparx\SmartSearch
* @author Sparx
* @copyright Copyright (c) 2015, Sparx. All rights reserved
*/
class SearchDataProvider implements DataProviderInterface
{
const PRODUCTS_NUMBER_IN_SUGGEST = 7;

/** @var QueryFactory */
protected $queryFactory;

/** @var ItemFactory */
protected $itemFactory;

/** @var \Magento\Framework\Api\Search\SearchInterface */
protected $fullTextSearchApi;

/** @var FullTextSearchCriteriaFactory */
protected $fullTextSearchCriteriaFactory;

/** @var FilterGroupBuilder */
protected $searchFilterGroupBuilder;

/** @var FilterBuilder */
protected $filterBuilder;

/** @var ProductRepositoryInterface */
protected $productRepository;

/** @var SearchCriteriaBuilder */
protected $searchCriteriaBuilder;

/**
* @var StoreManagerInterface
*/
protected $storeManager;

/**
* @var PriceCurrencyInterface
*/
protected $priceCurrency;

/**
* @var ProductHelper
*/
protected $productHelper;

/** @var \Magento\Catalog\Helper\Image */
protected $imageHelper;

/**
* Initialize dependencies.
*
* @param QueryFactory $queryFactory
* @param ItemFactory $itemFactory
* @param FullTextSearchApi $search
* @param FullTextSearchCriteriaFactory $searchCriteriaFactory
* @param FilterGroupBuilder $searchFilterGroupBuilder
* @param FilterBuilder $filterBuilder
* @param ProductRepositoryInterface $productRepository
* @param SearchCriteriaBuilder $searchCriteriaBuilder
* @param StoreManagerInterface $storeManager
* @param \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency
* @param \Magento\Catalog\Helper\Image $imageHelper
*/
public function __construct(
QueryFactory $queryFactory,
ItemFactory $itemFactory,
FullTextSearchApi $search,
FullTextSearchCriteriaFactory $searchCriteriaFactory,
FilterGroupBuilder $searchFilterGroupBuilder,
FilterBuilder $filterBuilder,
ProductRepositoryInterface $productRepository,
SearchCriteriaBuilder $searchCriteriaBuilder,
StoreManagerInterface $storeManager,
PriceCurrencyInterface $priceCurrency,
Image $imageHelper
)
{
$this->queryFactory = $queryFactory;
$this->itemFactory = $itemFactory;
$this->fullTextSearchApi = $search;
$this->fullTextSearchCriteriaFactory = $searchCriteriaFactory;
$this->filterBuilder = $filterBuilder;
$this->searchFilterGroupBuilder = $searchFilterGroupBuilder;
$this->productRepository = $productRepository;
$this->searchCriteriaBuilder = $searchCriteriaBuilder;
$this->storeManager = $storeManager;
$this->priceCurrency = $priceCurrency;
$this->imageHelper = $imageHelper;
}

/**
* getItems method
*
* @return array
*/
public function getItems()
{
$result = [ ];
$query = $this->queryFactory->get()->getQueryText();
$productIds = $this->searchProductsFullText($query);

// Check if products are found
if ( $productIds )
{
$searchCriteria = $this->searchCriteriaBuilder->addFilter('entity_id', $productIds, 'in')->create();
$products = $this->productRepository->getList($searchCriteria);

foreach ( $products->getItems() as $product )
{
$childProductimage = array();
$image = $this->imageHelper->init($product, 'product_page_image_small')->getUrl();
$_children = $product->getTypeInstance()->getUsedProducts($product);
foreach ($_children as $child){
$childProductimage[] = $this->imageHelper->init($child, 'product_page_image_small')->getUrl();
}

// if(!$image){
$image = $childProductimage[0];
// }
$resultItem = $this->itemFactory->create([
'type' => $product->getTypeId(),
'title' => $product->getName(),
'price' => $this->priceCurrency->format($product->getPriceInfo()->getPrice('regular_price')->getAmount()->getValue(),false),
'special_price' => $this->priceCurrency->format($product->getPriceInfo()->getPrice('special_price')->getAmount()->getValue(),false),
'has_special_price' => $product->getSpecialPrice() > 0 ? true : false,
'image' => $image,
'url' => $product->getProductUrl()
]);
$result[] = $resultItem;
}
}

return $result;
}

/**
* Perform full text search and find IDs of matching products.
*
* @param $query
*
* @return array
*/
protected function searchProductsFullText($query)
{
$searchCriteria = $this->fullTextSearchCriteriaFactory->create();

/** To get list of available request names see Magento/CatalogSearch/etc/search_request.xml */
$searchCriteria->setRequestName('quick_search_container');
$filter = $this->filterBuilder->setField('search_term')->setValue($query)->setConditionType('like')->create();
$filterGroup = $this->searchFilterGroupBuilder->addFilter($filter)->create();
$currentPage = 1;
$searchCriteria->setFilterGroups([ $filterGroup ])
->setCurrentPage($currentPage)
->setPageSize(self::PRODUCTS_NUMBER_IN_SUGGEST);
$searchResults = $this->fullTextSearchApi->search($searchCriteria);
$productIds = [ ];

/**
* Full text search returns document IDs (in this case product IDs),
* so to get products information we need to load them using filtration by these IDs
*/
foreach ( $searchResults->getItems() as $searchDocument )
{
$productIds[] = $searchDocument->getId();
}

return $productIds;
}
}

 

Step 5: Magento 2 SmartSearch FormMini

Here we can add overrided the form.mini.phtml file in default.xml file.

app/code/Wishusucess/SmartSearch/view/frontend/layout/default.xml

<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<head>
<css src="Sparx_SmartSearch::css/module.css"/>
</head>
<body> 
<referenceContainer name="header-wrapper">
<referenceBlock name="top.search">
<arguments>
<argument name="template" xsi:type="string">Wishusucess/SmartSeach/templates/form.mini.phtml</argument>
</arguments>
</referenceBlock>
</referenceContainer>
</body>
</page>

 

Step 6:

app/code/Wishusucess/SmartSearch/view/frontend/templates/form.mini.phtml

<?php
/*
* Author: Hemant Singh
* Website URL: http://www.wishusucess.com/
*/
$helper = $this->helper('Magento\Search\Helper\Data');
?>
<div class="has-toggle search-sections">

<div class="inner-toggle clearfix">
<div class="block block-search">
<div class="block block-title"><strong><?php echo __('Search'); ?></strong></div>
<div class="block block-content">
<form class="form minisearch" id="search_mini_form" action="<?php echo $helper->getResultUrl() ?>" method="get">
<div class="field search">
<div class="control">
<input id="search"
data-mage-init='{"quickSearch":{
"formSelector":"#search_mini_form",
"url":"<?php echo $this->getUrl('search/ajax/suggest'); ?>",
"destinationSelector":"#search_autocomplete"}
}'
type="text"
name="<?php echo $helper->getQueryParamName() ?>"
value="<?php echo $helper->getEscapedQueryText() ?>"
placeholder="<?php echo __('Search entire store here...'); ?>"
class="input-text"
autocomplete="off"
onfocus=this.value=''/>
<div id="search_autocomplete" class="search-autocomplete"></div>

<?php echo $this->getChildHtml() ?>
</div>
<!-- .control -->

<button type="submit" value="Zoeken" class="btn btn-search"><?php echo __('Search'); ?></button>

</div> <!-- .field .search -->
<div class="actions"></div>
</form>
</div>
<!-- .block-content -->
</div>
<!-- .block-search -->
</div>
<!-- .inner-toggle -->
</div>
<!-- .search-sections -->

 

Step 7:

app/code/Wishusucess/SmartSearch/view/frontend/requirejs-config.js

var config = {
map: {
'*': {
'quickSearch':'Wishusucess_SmartSearch/form-mini'
}
}
};

 

Step 8:

app/code/Wishusucess/SmartSearch/view/frontend/web/form-mini.js

/**
* Copyright © 2015 Magento. All rights reserved.
* See COPYING.txt for license details.
*/
/*jshint browser:true jquery:true*/
define([
'jquery',
'underscore',
'mage/template',
'jquery/ui',
'mage/translate'
], function ($, _, mageTemplate) {
'use strict';

/**
* Check wether the incoming string is not empty or if doesn't consist of spaces.
*
* @param {String} value - Value to check.
* @returns {Boolean}
*/
function isEmpty(value) {
return (value.length === 0) || (value == null) || /^\s+$/.test(value);
}

$.widget('mage.quickSearch', {
options: {
autocomplete: 'off',
minSearchLength: 3,
responseFieldElements: 'ul li',
selectClass: 'selected',
template:
'<div class="c-smartsearch__product col-8-8">' +

'<a class="c-smartsearch__image col-2-8" href="<%- data.url %>">' +
'<img src="<%- data.image %>">' +
'</a>' +

'<div class="c-smartsearch__description col-6-8 last">' +

'<a class="c-smartsearch__title h4" href="<%- data.url %>">' +
'<%- data.title %>' +
'</a>' +

'<div class="c-smartsearch__price">' +
'<span>' + $.mage.__("") + ' <span class="<% if (data.has_special_price) { %> u-strike-through <% } %> ">' + '<%- data.price %></span></span>' +
'</div>' +

'<% if (data.has_special_price) { %>' +
'<div class="c-smartsearch__sale">' +
'<span class="c-smartsearch__sale--accent"> <%- data.special_price %></span>' +
'</div>' +
'<% } %>'
+
'</div> <!-- .c-smartsearch__product col-8-8 -->'
,
submitBtn: 'button[type="submit"]',
searchLabel: '[data-role=minisearch-label]'
},
_create: function () {
this.responseList = {
indexList: null,
selected: null
};
this.autoComplete = $(this.options.destinationSelector);
this.searchForm = $(this.options.formSelector);
this.submitBtn = this.searchForm.find(this.options.submitBtn)[0];
this.searchLabel = $(this.options.searchLabel);

_.bindAll(this, '_onKeyDown', '_onPropertyChange', '_onSubmit');

this.submitBtn.disabled = true;

this.element.attr('autocomplete', this.options.autocomplete);

this.element.on('blur', $.proxy(function () {

setTimeout($.proxy(function () {
if (this.autoComplete.is(':hidden')) {
this.searchLabel.removeClass('active');
}
this.autoComplete.hide();
this._updateAriaHasPopup(false);
}, this), 250);
}, this));

this.element.trigger('blur');

this.element.on('focus', $.proxy(function () {
this.searchLabel.addClass('active');

if($('#search').val().length >= 3) {
$('#search_autocomplete').css('display', 'block');
}
}, this));
this.element.on('keydown', this._onKeyDown);
this.element.on('input propertychange', this._onPropertyChange);

this.searchForm.on('submit', $.proxy(function() {
this._onSubmit();
this._updateAriaHasPopup(false);
}, this));
},
/**
* @private
* @return {Element} The first element in the suggestion list.
*/
_getFirstVisibleElement: function () {
return this.responseList.indexList ? this.responseList.indexList.first() : false;
},

/**
* @private
* @return {Element} The last element in the suggestion list.
*/
_getLastElement: function () {
return this.responseList.indexList ? this.responseList.indexList.last() : false;
},

/**
* @private
* @param {Boolean} show Set attribute aria-haspopup to "true/false" for element.
*/
_updateAriaHasPopup: function(show) {
if (show) {
this.element.attr('aria-haspopup', 'true');
} else {
this.element.attr('aria-haspopup', 'false');
}
},

/**
* Clears the item selected from the suggestion list and resets the suggestion list.
* @private
* @param {Boolean} all - Controls whether to clear the suggestion list.
*/
_resetResponseList: function (all) {
this.responseList.selected = null;

if (all === true) {
this.responseList.indexList = null;
}
},

/**
* Executes when the search box is submitted. Sets the search input field to the
* value of the selected item.
* @private
* @param {Event} e - The submit event
*/
_onSubmit: function (e) {
var value = this.element.val();

if (isEmpty(value)) {
e.preventDefault();
}

if (this.responseList.selected) {
this.element.val(this.responseList.selected.find('.qs-option-name').text());
}
},

/**
* Executes when keys are pressed in the search input field. Performs specific actions
* depending on which keys are pressed.
* @private
* @param {Event} e - The key down event
* @return {Boolean} Default return type for any unhandled keys
*/
_onKeyDown: function (e) {
var keyCode = e.keyCode || e.which;

switch (keyCode) {
case $.ui.keyCode.HOME:
// $('#search').removeClass('loading');
this._getFirstVisibleElement().addClass(this.options.selectClass);
this.responseList.selected = this._getFirstVisibleElement();
break;
case $.ui.keyCode.END:
// $('#search').removeClass('loading');
this._getLastElement().addClass(this.options.selectClass);
this.responseList.selected = this._getLastElement();
break;
case $.ui.keyCode.ESCAPE:
this._resetResponseList(true);
// $('#search').removeClass('loading');
this.autoComplete.hide();
break;
case $.ui.keyCode.ENTER:
// $('#search').removeClass('loading');
this.searchForm.trigger('submit');
break;
case $.ui.keyCode.DOWN:

if (this.responseList.indexList) {
if (!this.responseList.selected) {
this._getFirstVisibleElement().addClass(this.options.selectClass);
this.responseList.selected = this._getFirstVisibleElement();
}
else if (!this._getLastElement().hasClass(this.options.selectClass)) {
this.responseList.selected = this.responseList.selected.removeClass(this.options.selectClass).next().addClass(this.options.selectClass);
} else {
this.responseList.selected.removeClass(this.options.selectClass);
this._getFirstVisibleElement().addClass(this.options.selectClass);
this.responseList.selected = this._getFirstVisibleElement();
}
this.element.val(this.responseList.selected.find('.qs-option-name').text());
this.element.attr('aria-activedescendant', this.responseList.selected.attr('id'));
}
break;
case $.ui.keyCode.UP:

if (this.responseList.indexList !== null) {
if (!this._getFirstVisibleElement().hasClass(this.options.selectClass)) {
this.responseList.selected = this.responseList.selected.removeClass(this.options.selectClass).prev().addClass(this.options.selectClass);

} else {
this.responseList.selected.removeClass(this.options.selectClass);
this._getLastElement().addClass(this.options.selectClass);
this.responseList.selected = this._getLastElement();
}
this.element.val(this.responseList.selected.find('.qs-option-name').text());
this.element.attr('aria-activedescendant', this.responseList.selected.attr('id'));
}
break;
default:
return true;
}
},

/**
* Executes when the value of the search input field changes. Executes a GET request
* to populate a suggestion list based on entered text. Handles click (select), hover,
* and mouseout events on the populated suggestion list dropdown.
* @private
*/
_onPropertyChange: function () {
var searchField = this.element,
clonePosition = {
position: 'absolute',
width: '100%'
// Removed to fix display issues
// left: searchField.offset().left,
// top: searchField.offset().top + searchField.outerHeight(),
//width: searchField.outerWidth()
},
source = this.options.template,
template = mageTemplate(source),
dropdown = $('<div class="c-smartsearch"><div class="c-smartsearch__overlay"></div><div class="c-smartsearch__wrapper"><div class="c-smartsearch__wrapper--inner"><div class="c-smartsearch__content clearfix"></div>'),
value = this.element.val();

this.submitBtn.disabled = isEmpty(value);

if (value.length >= parseInt(this.options.minSearchLength, 10)) {

$('#search_mini_form').addClass('is-loading');

if(this.request) {
this.request.abort();
}

this.request = $.get(this.options.url, {q: value}, $.proxy(function (data) {

// Check if SmartSearch returned results
if( ! data.length)
dropdown.find('.c-smartsearch__content').append('<div class="c-smartsearch__product col-8-8">' + $.mage.__('No products found.') + '</div>');

$.each(data, function(index, element) {
element.index = index;

var html = template({
data: element
});
dropdown.find('.c-smartsearch__content').append(html);
});

this.responseList.indexList = this.autoComplete.html(dropdown)
.css(clonePosition)
.show()
.find(this.options.responseFieldElements + ':visible');

$('#search_mini_form').removeClass('is-loading');

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) {
this.responseList.selected = $(e.target);
this.searchForm.trigger('submit');
}.bind(this))
.on('mouseenter mouseleave', function (e) {
this.responseList.indexList.removeClass(this.options.selectClass);
$(e.target).addClass(this.options.selectClass);
this.responseList.selected = $(e.target);
this.element.attr('aria-activedescendant', $(e.target).attr('id'));
}.bind(this))
.on('mouseout', function (e) {
if (!this._getLastElement() && this._getLastElement().hasClass(this.options.selectClass)) {
$(e.target).removeClass(this.options.selectClass);
this._resetResponseList(false);
}
}.bind(this));
}, this));
} else {
this._resetResponseList(true);
this.autoComplete.hide();
this._updateAriaHasPopup(false);
this.element.removeAttr('aria-activedescendant');
}
}
});

return $.mage.quickSearch;
});

 

Step 9:

Now design your Magento 2 catalog search box and how that content should appear.

app/code/Wishusucess/SmartSearch/view/frontend/web/css/module.less

/** ================================================================
* Author: Hemant Singh
* Website URL: http://www.wishusucess.com/
* ============================================================= */
#search.loading {

}
#search_autocomplete {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: visible;
}
.c-smartsearch {
text-transform: capitalize;
}
.c-smartsearch__overlay {
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 998;
}
.c-smartsearch__wrapper {
position: absolute;
width: 99.6%;
top: 32px;
z-index: 999;
left: 7px;
border: 1px solid #ededed;
box-shadow: 0px 1px 4px 0px #ededed;
background: #FFFFFF;
}
.c-smartsearch__wrapper--inner {
width: 100%;
max-width: 460px;
margin: 0 auto;
}
.c-smartsearch__content {
background-color: white;
}
.c-smartsearch__product {
border-top: 1px solid #f2f2f2;
padding: 15px 15px;
}
.c-smartsearch__title {
text-decoration: none;
float: left;
clear: both;
}
.c-smartsearch {
.c-smartsearch__sale--accent {
color: #3a3a3a;
}
.c-smartsearch__price {
float: left;

> span {
padding-right: 2px;
}
}
.u-strike-through {
text-decoration: line-through;
}
.col-8-8 {
width: 89%;
float: left;
margin-left: 0;
margin-right: 0;
}
.col-2-8 {
width: 22.89157%;
float: left;
margin-right: 2.81124%;
}
.col-6-8 {
width: 72%;
float: right;
margin-right: 0;
margin-top: -4px;
}
}

@media screen and (max-width: 1023px) {
.c-smartsearch__wrapper--inner {
max-width: 100% !important;
}

.c-smartsearch__wrapper {
top: 50px !important;
}

.c-smartsearch__image {
text-align: center;

img {
max-height: 150px;
width: auto;
}
}
}

 

Now, Run Following Command:

php bin/magento setup:upgrade

php bin/magento setup:di:compile

php bin/magento setup:static-content:deplpy -f

php bin/magento cache:clean

 

Download Link:

Wishusucess Smart Search Magento 2 Extension

 

Related Post:

Wishusucess AdminMenu: How to Create Magento 2 Admin Menu Module

Search AutoComplete: Magento 2 Module Add All Category for Search

 

Recommended Post:

Magento 2.4 Installation Guide: How to Install Magento 2.4.2

Magento Store: Best 36 Magento Websites Example in The World