feat: Search UI
- Search UI with dropdown results
- Client class to handle Product Search actions and results
- Integrated Search bar into all-products and item group pages
- Run db search without redisearch
- Cleanup: [Search] change decorator names and variables
- Sider fixes
diff --git a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py
index cca8df5..8d6cc74 100644
--- a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py
+++ b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.py
@@ -6,7 +6,7 @@
from frappe.utils import cint, comma_and
from frappe import _, msgprint
from frappe.model.document import Document
-from frappe.utils import get_datetime, get_datetime_str, now_datetime, unique
+from frappe.utils import unique
from erpnext.e_commerce.website_item_indexing import create_website_items_index, ALLOWED_INDEXABLE_FIELDS_SET, is_search_module_loaded
class ShoppingCartSetupError(frappe.ValidationError): pass
@@ -59,11 +59,8 @@
if not self.search_index_fields:
return
- # Clean up
- # Remove whitespaces
fields = self.search_index_fields.replace(' ', '')
- # Remove extra ',' and remove duplicates
- fields = unique(fields.strip(',').split(','))
+ fields = unique(fields.strip(',').split(',')) # Remove extra ',' and remove duplicates
# All fields should be indexable
if not (set(fields).issubset(ALLOWED_INDEXABLE_FIELDS_SET)):
diff --git a/erpnext/e_commerce/product_grid.js b/erpnext/e_commerce/product_grid.js
index a716efa..bd7a568 100644
--- a/erpnext/e_commerce/product_grid.js
+++ b/erpnext/e_commerce/product_grid.js
@@ -144,6 +144,8 @@
${ __('Add to Cart') }
</div>
`;
+ } else {
+ return ``;
}
}
};
\ No newline at end of file
diff --git a/erpnext/e_commerce/product_list.js b/erpnext/e_commerce/product_list.js
index 3aa6e8c..d5ba6f5 100644
--- a/erpnext/e_commerce/product_list.js
+++ b/erpnext/e_commerce/product_list.js
@@ -153,6 +153,8 @@
${ __('Add to Cart') }
</div>
`;
+ } else {
+ return ``;
}
}
diff --git a/erpnext/e_commerce/product_search.js b/erpnext/e_commerce/product_search.js
new file mode 100644
index 0000000..4f8b028
--- /dev/null
+++ b/erpnext/e_commerce/product_search.js
@@ -0,0 +1,226 @@
+erpnext.ProductSearch = class {
+ constructor() {
+ this.MAX_RECENT_SEARCHES = 4;
+ this.searchBox = $("#search-box");
+
+ this.setupSearchDropDown();
+ this.bindSearchAction();
+ }
+
+ setupSearchDropDown() {
+ this.search_area = $("#dropdownMenuSearch");
+ this.setupSearchResultContainer();
+ this.setupProductsContainer();
+ this.setupCategoryRecentsContainer();
+ this.populateRecentSearches();
+ }
+
+ bindSearchAction() {
+ let me = this;
+
+ this.searchBox.on("focus", (e) => {
+ this.search_dropdown.removeClass("hidden");
+ });
+
+ this.searchBox.on("focusout", (e) => {
+ this.search_dropdown.addClass("hidden");
+ });
+
+ this.searchBox.on("input", (e) => {
+ let query = e.target.value;
+
+ if (query.length < 3 || !query.length) return;
+
+ // Populate recent search chips
+ me.setRecentSearches(query);
+
+ // Fetch and populate product results
+ frappe.call({
+ method: "erpnext.templates.pages.product_search.search",
+ args: {
+ query: query
+ },
+ callback: (data) => {
+ me.populateResults(data);
+ }
+ });
+
+ // Populate categories
+ if (me.category_container) {
+ frappe.call({
+ method: "erpnext.templates.pages.product_search.get_category_suggestions",
+ args: {
+ query: query
+ },
+ callback: (data) => {
+ me.populateCategoriesList(data)
+ }
+ });
+ }
+
+ this.search_dropdown.removeClass("hidden");
+ });
+ }
+
+ setupSearchResultContainer() {
+ this.search_dropdown = this.search_area.append(`
+ <div class="overflow-hidden shadow dropdown-menu w-100 hidden"
+ id="search-results-container"
+ aria-labelledby="dropdownMenuSearch"
+ style="display: flex;">
+ </div>
+ `).find("#search-results-container");
+ }
+
+ setupProductsContainer() {
+ let $products_section = this.search_dropdown.append(`
+ <div class="col-7 mr-2 mt-1"
+ id="product-results"
+ style="border-right: 1px solid var(--gray-200);">
+ </div>
+ `).find("#product-results");
+
+ this.products_container = $products_section.append(`
+ <div id="product-scroll" style="overflow: scroll; max-height: 300px">
+ <div class="mt-6 w-100 text-muted" style="font-weight: 400; text-align: center;">
+ ${ __("Type something ...") }
+ </div>
+ </div>
+ `).find("#product-scroll");
+ }
+
+ setupCategoryRecentsContainer() {
+ let $category_recents_section = $("#search-results-container").append(`
+ <div id="category-recents-container"
+ class="col-5 mt-2 h-100"
+ style="margin-left: -15px;">
+ </div>
+ `).find("#category-recents-container");
+
+ this.category_container = $category_recents_section.append(`
+ <div class="category-container">
+ <div class="mb-2"
+ style="border-bottom: 1px solid var(--gray-200);">
+ ${ __("Categories") }
+ </div>
+ <div class="categories">
+ <span class="text-muted" style="font-weight: 400;"> ${ __('No results') } <span>
+ </div>
+ </div>
+ `).find(".categories");
+
+ let $recents_section = $("#category-recents-container").append(`
+ <div class="mb-2 mt-4 recent-searches">
+ <div style="border-bottom: 1px solid var(--gray-200);">
+ ${ __("Recent") }
+ </div>
+ </div>
+ `).find(".recent-searches");
+
+ this.recents_container = $recents_section.append(`
+ <div id="recent-chips" style="padding: 1rem 0;">
+ </div>
+ `).find("#recent-chips");
+ }
+
+ getRecentSearches() {
+ return JSON.parse(localStorage.getItem("recent_searches") || "[]");
+ }
+
+ attachEventListenersToChips() {
+ let me = this;
+ const chips = $(".recent-chip");
+ window.chips = chips;
+
+ for (let chip of chips) {
+ chip.addEventListener("click", () => {
+ me.searchBox[0].value = chip.innerText;
+
+ // Start search with `recent query`
+ me.searchBox.trigger("input");
+ me.searchBox.focus();
+ });
+ }
+ }
+
+ setRecentSearches(query) {
+ let recents = this.getRecentSearches();
+ if (recents.length >= this.MAX_RECENT_SEARCHES) {
+ // Remove the `first` query
+ recents.splice(0, 1);
+ }
+
+ if (recents.indexOf(query) >= 0) {
+ return;
+ }
+
+ recents.push(query);
+ localStorage.setItem("recent_searches", JSON.stringify(recents));
+
+ this.populateRecentSearches();
+ }
+
+ populateRecentSearches() {
+ let recents = this.getRecentSearches();
+
+ if (!recents.length) {
+ return;
+ }
+
+ let html = "";
+ recents.forEach((key) => {
+ html += `<button class="btn btn-sm recent-chip mr-1 mb-2">${ key }</button>`;
+ });
+
+ this.recents_container.html(html);
+ this.attachEventListenersToChips();
+ }
+
+ populateResults(data) {
+ if (data.message.results.length === 0) {
+ this.products_container.html('No results');
+ return;
+ }
+
+ let html = "";
+ let search_results = data.message.results;
+
+ search_results.forEach((res) => {
+ html += `
+ <div class="dropdown-item" style="display: flex;">
+ <img class="item-thumb col-2" src=${res.thumbnail || 'img/placeholder.png'} />
+ <div class="col-9" style="white-space: normal;">
+ <a href="/${res.route}">${res.web_item_name}</a><br>
+ <span class="brand-line">${res.brand ? "by " + res.brand : ""}</span>
+ </div>
+ </div>
+ `;
+ });
+
+ this.products_container.html(html);
+ }
+
+ populateCategoriesList(data) {
+ if (data.message.results.length === 0) {
+ let empty_html = `
+ <span class="text-muted" style="font-weight: 400;">
+ ${__('No results')}
+ </span>
+ `;
+ this.category_container.html(empty_html);
+ return;
+ }
+
+ let html = ""
+ let search_results = data.message.results
+ search_results.forEach((category) => {
+ html += `
+ <div class="mb-2" style="font-weight: 400;">
+ <a href="/${category.route}">${category.name}</a>
+ </div>
+ `;
+ })
+
+ this.category_container.html(html);
+ }
+}
\ No newline at end of file
diff --git a/erpnext/e_commerce/product_view.js b/erpnext/e_commerce/product_view.js
index 4e1c23c..3a2cdae 100644
--- a/erpnext/e_commerce/product_view.js
+++ b/erpnext/e_commerce/product_view.js
@@ -402,4 +402,4 @@
}
return exists ? obj : undefined;
}
-}
\ No newline at end of file
+};
\ No newline at end of file
diff --git a/erpnext/e_commerce/website_item_indexing.py b/erpnext/e_commerce/website_item_indexing.py
index 3b82a32..3270101 100644
--- a/erpnext/e_commerce/website_item_indexing.py
+++ b/erpnext/e_commerce/website_item_indexing.py
@@ -1,39 +1,10 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import frappe
from frappe.utils.redis_wrapper import RedisWrapper
+from redisearch import (Client, AutoCompleter, Suggestion, IndexDefinition, TextField, TagField)
-from redisearch import (
- Client, AutoCompleter,
- Suggestion, IndexDefinition,
- TextField, TagField
- )
-
-def is_search_module_loaded():
- cache = frappe.cache()
- out = cache.execute_command('MODULE LIST')
-
- parsed_output = " ".join(
- (" ".join([s.decode() for s in o if not isinstance(s, int)]) for o in out)
- )
-
- return "search" in parsed_output
-
-# Decorator for checking wether Redisearch is there or not
-def redisearch_decorator(function):
- def wrapper(*args, **kwargs):
- if is_search_module_loaded():
- func = function(*args, **kwargs)
- return func
- return
-
- return wrapper
-
-def make_key(key):
- return "{0}|{1}".format(frappe.conf.db_name, key).encode('utf-8')
-
-# GLOBAL CONSTANTS
WEBSITE_ITEM_INDEX = 'website_items_index'
WEBSITE_ITEM_KEY_PREFIX = 'website_item:'
WEBSITE_ITEM_NAME_AUTOCOMPLETE = 'website_items_name_dict'
@@ -48,7 +19,30 @@
'web_long_description'
}
-@redisearch_decorator
+def is_search_module_loaded():
+ cache = frappe.cache()
+ out = cache.execute_command('MODULE LIST')
+
+ parsed_output = " ".join(
+ (" ".join([s.decode() for s in o if not isinstance(s, int)]) for o in out)
+ )
+
+ return "search" in parsed_output
+
+# Decorator for checking wether Redisearch is there or not
+def if_redisearch_loaded(function):
+ def wrapper(*args, **kwargs):
+ if is_search_module_loaded():
+ func = function(*args, **kwargs)
+ return func
+ return
+
+ return wrapper
+
+def make_key(key):
+ return "{0}|{1}".format(frappe.conf.db_name, key).encode('utf-8')
+
+@if_redisearch_loaded
def create_website_items_index():
'''Creates Index Definition'''
# CREATE index
@@ -57,21 +51,20 @@
# DROP if already exists
try:
client.drop_index()
- except:
+ except Exception:
pass
-
idx_def = IndexDefinition([make_key(WEBSITE_ITEM_KEY_PREFIX)])
# Based on e-commerce settings
idx_fields = frappe.db.get_single_value(
- 'E Commerce Settings',
+ 'E Commerce Settings',
'search_index_fields'
).split(',')
if 'web_item_name' in idx_fields:
idx_fields.remove('web_item_name')
-
+
idx_fields = list(map(to_search_field, idx_fields))
client.create_index(
@@ -88,26 +81,25 @@
return TextField(field)
-@redisearch_decorator
+@if_redisearch_loaded
def insert_item_to_index(website_item_doc):
# Insert item to index
key = get_cache_key(website_item_doc.name)
- r = frappe.cache()
+ cache = frappe.cache()
web_item = create_web_item_map(website_item_doc)
for k, v in web_item.items():
- super(RedisWrapper, r).hset(make_key(key), k, v)
+ super(RedisWrapper, cache).hset(make_key(key), k, v)
insert_to_name_ac(website_item_doc.web_item_name, website_item_doc.name)
-@redisearch_decorator
+@if_redisearch_loaded
def insert_to_name_ac(web_name, doc_name):
ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=frappe.cache())
ac.add_suggestions(Suggestion(web_name, payload=doc_name))
def create_web_item_map(website_item_doc):
fields_to_index = get_fields_indexed()
-
web_item = {}
for f in fields_to_index:
@@ -115,59 +107,58 @@
return web_item
-@redisearch_decorator
+@if_redisearch_loaded
def update_index_for_item(website_item_doc):
# Reinsert to Cache
insert_item_to_index(website_item_doc)
define_autocomplete_dictionary()
-@redisearch_decorator
+@if_redisearch_loaded
def delete_item_from_index(website_item_doc):
- r = frappe.cache()
+ cache = frappe.cache()
key = get_cache_key(website_item_doc.name)
-
+
try:
- r.delete(key)
+ cache.delete(key)
except:
return False
delete_from_ac_dict(website_item_doc)
-
return True
-@redisearch_decorator
+@if_redisearch_loaded
def delete_from_ac_dict(website_item_doc):
'''Removes this items's name from autocomplete dictionary'''
- r = frappe.cache()
- name_ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=r)
+ cache = frappe.cache()
+ name_ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=cache)
name_ac.delete(website_item_doc.web_item_name)
-@redisearch_decorator
+@if_redisearch_loaded
def define_autocomplete_dictionary():
"""Creates an autocomplete search dictionary for `name`.
- Also creats autocomplete dictionary for `categories` if
- checked in E Commerce Settings"""
+ Also creats autocomplete dictionary for `categories` if
+ checked in E Commerce Settings"""
- r = frappe.cache()
- name_ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=r)
- cat_ac = AutoCompleter(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE), conn=r)
+ cache = frappe.cache()
+ name_ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=cache)
+ cat_ac = AutoCompleter(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE), conn=cache)
ac_categories = frappe.db.get_single_value(
- 'E Commerce Settings',
+ 'E Commerce Settings',
'show_categories_in_search_autocomplete'
)
-
+
# Delete both autocomplete dicts
try:
- r.delete(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE))
- r.delete(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE))
+ cache.delete(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE))
+ cache.delete(make_key(WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE))
except:
return False
-
+
items = frappe.get_all(
- 'Website Item',
- fields=['web_item_name', 'item_group'],
- filters={"published": True}
+ 'Website Item',
+ fields=['web_item_name', 'item_group'],
+ filters={"published": 1}
)
for item in items:
@@ -177,21 +168,21 @@
return True
-@redisearch_decorator
+@if_redisearch_loaded
def reindex_all_web_items():
items = frappe.get_all(
- 'Website Item',
- fields=get_fields_indexed(),
+ 'Website Item',
+ fields=get_fields_indexed(),
filters={"published": True}
)
- r = frappe.cache()
+ cache = frappe.cache()
for item in items:
web_item = create_web_item_map(item)
key = make_key(get_cache_key(item.name))
for k, v in web_item.items():
- super(RedisWrapper, r).hset(key, k, v)
+ super(RedisWrapper, cache).hset(key, k, v)
def get_cache_key(name):
name = frappe.scrub(name)
@@ -199,7 +190,7 @@
def get_fields_indexed():
fields_to_index = frappe.db.get_single_value(
- 'E Commerce Settings',
+ 'E Commerce Settings',
'search_index_fields'
).split(',')
diff --git a/erpnext/public/build.json b/erpnext/public/build.json
index 51082bd..553082f 100644
--- a/erpnext/public/build.json
+++ b/erpnext/public/build.json
@@ -70,6 +70,7 @@
"js/e-commerce.min.js": [
"e_commerce/product_view.js",
"e_commerce/product_grid.js",
- "e_commerce/product_list.js"
+ "e_commerce/product_list.js",
+ "e_commerce/product_search.js"
]
}
diff --git a/erpnext/public/js/shopping_cart.js b/erpnext/public/js/shopping_cart.js
index 6b42987..40d58d1 100644
--- a/erpnext/public/js/shopping_cart.js
+++ b/erpnext/public/js/shopping_cart.js
@@ -199,7 +199,7 @@
},
freeze() {
- if (window.location.pathname !== "/cart") return
+ if (window.location.pathname !== "/cart") return;
if (!$('#freeze').length) {
let freeze = $('<div id="freeze" class="modal-backdrop fade"></div>')
diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss
index 8048f76..7128c02 100644
--- a/erpnext/public/scss/shopping_cart.scss
+++ b/erpnext/public/scss/shopping_cart.scss
@@ -421,7 +421,7 @@
.total-discount {
font-size: var(--text-base);
- color: var(--primary-color);
+ color: var(--primary-color) !important;
}
#page-cart {
@@ -529,6 +529,7 @@
height: 22px;
background-color: var(--gray-200);
float: right;
+ cursor: pointer;
}
.remove-cart-item-logo {
@@ -865,3 +866,14 @@
background-color: var(--gray-100);
height: 100%;
}
+
+.item-thumb {
+ height: 50px;
+ max-width: 80px;
+ min-width: 80px;
+ object-fit: cover;
+}
+
+.brand-line {
+ color: gray;
+}
diff --git a/erpnext/templates/generators/item_group.html b/erpnext/templates/generators/item_group.html
index 20ad7fb..0b2fb03 100644
--- a/erpnext/templates/generators/item_group.html
+++ b/erpnext/templates/generators/item_group.html
@@ -2,7 +2,16 @@
{% extends "templates/web.html" %}
{% block header %}
-<!-- <h2>{{ title }}</h2> -->
+<div class="row mb-6" style="width: 65vw">
+ <div class="mb-6 col-4 order-1">{{ title }}</div>
+
+ <div class="input-group mb-6 col-8 order-2">
+ <div class="dropdown w-100" id="dropdownMenuSearch">
+ <input type="search" name="query" id="search-box" class="form-control" placeholder="Search for products..." aria-label="Product" aria-describedby="button-addon2">
+ <!-- Results dropdown rendered in product_search.js -->
+ </div>
+ </div>
+</div>
{% endblock header %}
{% block script %}
@@ -19,7 +28,7 @@
<div class="item-group-content" itemscope itemtype="http://schema.org/Product"
data-item-group="{{ name }}">
<div class="item-group-slideshow">
- {% if slideshow %}<!-- slideshow -->
+ {% if slideshow %} <!-- slideshow -->
{{ web_block(
"Hero Slider",
values=slideshow,
@@ -28,8 +37,8 @@
add_bottom_padding=0,
) }}
{% endif %}
- <h2 class="mt-3">{{ title }}</h2>
- {% if description %}<!-- description -->
+
+ {% if description %} <!-- description -->
<div class="item-group-description text-muted mb-5" itemprop="description">{{ description or ""}}</div>
{% endif %}
</div>
diff --git a/erpnext/templates/includes/cart.js b/erpnext/templates/includes/cart.js
index 28fe882..c766dfd 100644
--- a/erpnext/templates/includes/cart.js
+++ b/erpnext/templates/includes/cart.js
@@ -163,7 +163,7 @@
item_code: item_code,
qty: 0
});
- })
+ });
},
render_tax_row: function($cart_taxes, doc, shipping_rules) {
diff --git a/erpnext/templates/pages/product_search.py b/erpnext/templates/pages/product_search.py
index 12967fa..190abfd 100644
--- a/erpnext/templates/pages/product_search.py
+++ b/erpnext/templates/pages/product_search.py
@@ -7,7 +7,6 @@
from erpnext.setup.doctype.item_group.item_group import get_item_for_list_in_html
from erpnext.e_commerce.shopping_cart.product_info import set_product_info_for_website
-# For SEARCH -------
from redisearch import AutoCompleter, Client, Query
from erpnext.e_commerce.website_item_indexing import (
is_search_module_loaded,
@@ -16,7 +15,6 @@
WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE,
make_key
)
-# -----------------
no_cache = 1
@@ -36,30 +34,29 @@
def get_product_data(search=None, start=0, limit=12):
# limit = 12 because we show 12 items in the grid view
# base query
- query = """select I.name, I.item_name, I.item_code, I.route, I.image, I.website_image, I.thumbnail, I.item_group,
- I.description, I.web_long_description as website_description, I.is_stock_item,
- case when (S.actual_qty - S.reserved_qty) > 0 then 1 else 0 end as in_stock, I.website_warehouse,
- I.has_batch_no
- from `tabItem` I
- left join tabBin S on I.item_code = S.item_code and I.website_warehouse = S.warehouse
- where (I.show_in_website = 1)
- and I.disabled = 0
- and (I.end_of_life is null or I.end_of_life='0000-00-00' or I.end_of_life > %(today)s)"""
+ query = """
+ Select
+ web_item_name, item_name, item_code, brand, route,
+ website_image, thumbnail, item_group,
+ description, web_long_description as website_description,
+ website_warehouse, ranking
+ from `tabWebsite Item`
+ where published = 1
+ """
# search term condition
if search:
- query += """ and (I.web_long_description like %(search)s
- or I.description like %(search)s
- or I.item_name like %(search)s
- or I.name like %(search)s)"""
+ query += """ and (item_name like %(search)s
+ or web_item_name like %(search)s
+ or brand like %(search)s
+ or web_long_description like %(search)s)"""
search = "%" + cstr(search) + "%"
# order by
- query += """ order by I.weightage desc, in_stock desc, I.modified desc limit %s, %s""" % (cint(start), cint(limit))
+ query += """ order by ranking asc, modified desc limit %s, %s""" % (cint(start), cint(limit))
return frappe.db.sql(query, {
- "search": search,
- "today": nowdate()
+ "search": search
}, as_dict=1)
@frappe.whitelist(allow_guest=True)
@@ -112,11 +109,19 @@
@frappe.whitelist(allow_guest=True)
def get_category_suggestions(query):
- search_results = {"from_redisearch": True, "results": []}
+ search_results = {"results": []}
if not is_search_module_loaded():
- # Redisearch module not loaded
- search_results["from_redisearch"] = False
+ # Redisearch module not loaded, query db
+ categories = frappe.db.get_all(
+ "Item Group",
+ filters={
+ "name": ["like", "%{0}%".format(query)],
+ "show_in_website": 1
+ },
+ fields=["name", "route"]
+ )
+ search_results['results'] = categories
return search_results
if not query:
diff --git a/erpnext/www/all-products/index.html b/erpnext/www/all-products/index.html
index f89ee65..0a62bc4 100644
--- a/erpnext/www/all-products/index.html
+++ b/erpnext/www/all-products/index.html
@@ -3,35 +3,19 @@
{% block title %}{{ _('Products') }}{% endblock %}
{% block header %}
-<div class="mb-6">{{ _('Products') }}</div>
+<div class="row mb-6" style="width: 65vw">
+ <div class="mb-6 col-4 order-1">{{ _('Products') }}</div>
+
+ <div class="input-group mb-6 col-8 order-2">
+ <div class="dropdown w-100" id="dropdownMenuSearch">
+ <input type="search" name="query" id="search-box" class="form-control" placeholder="Search for products..." aria-label="Product" aria-describedby="button-addon2">
+ <!-- Results dropdown rendered in product_search.js -->
+ </div>
+ </div>
+</div>
{% endblock header %}
{% block page_content %}
-<!-- Old Search -->
-<!-- <div class="row" style="display: none;">
- <div class="col-8">
- <div class="input-group input-group-sm mb-3">
- <input type="search" class="form-control" placeholder="{{_('Search')}}"
- aria-label="{{_('Product Search')}}" aria-describedby="product-search"
- value="{{ frappe.sanitize_html(frappe.form_dict.search) or '' }}"
- >
- </div>
- </div>
-
- <div class="col-4 pl-0">
- <button class="btn btn-light btn-sm btn-block d-md-none"
- type="button"
- data-toggle="collapse"
- data-target="#product-filters"
- aria-expanded="false"
- aria-controls="product-filters"
- style="white-space: nowrap;"
- >
- {{ _('Toggle Filters') }}
- </button>
- </div>
-</div> -->
-
<div class="row">
<!-- Items section -->
<div id="product-listing" class="col-12 order-2 col-md-9 order-md-2 item-card-group-section">
@@ -40,11 +24,6 @@
<!-- Filters Section -->
<div class="col-12 order-1 col-md-3 order-md-1">
- {% if frappe.form_dict.start or frappe.form_dict.field_filters or frappe.form_dict.attribute_filters or frappe.form_dict.search %}
-
-
- {% endif %}
-
<div class="collapse d-md-block mr-4 filters-section" id="product-filters">
<div class="d-flex justify-content-between align-items-center mb-5 title-section">
<div class="mb-4 filters-title" > {{ _('Filters') }} </div>
diff --git a/erpnext/www/all-products/index.js b/erpnext/www/all-products/index.js
index ef8c210..e38514a 100644
--- a/erpnext/www/all-products/index.js
+++ b/erpnext/www/all-products/index.js
@@ -7,8 +7,10 @@
let view_type = "List View";
- // Render Product Views and setup Filters
+ // Render Product Views, Filters & Search
frappe.require('/assets/js/e-commerce.min.js', function() {
+ new erpnext.ProductSearch();
+
new erpnext.ProductView({
view_type: view_type,
products_section: $('#product-listing'),
diff --git a/erpnext/www/all-products/search.css b/erpnext/www/all-products/search.css
deleted file mode 100644
index 687532d..0000000
--- a/erpnext/www/all-products/search.css
+++ /dev/null
@@ -1,9 +0,0 @@
-.item-thumb {
- height: 50px;
- width: 50px;
- object-fit: cover;
-}
-
-.brand-line {
- color: gray;
-}
\ No newline at end of file
diff --git a/erpnext/www/all-products/search.html b/erpnext/www/all-products/search.html
deleted file mode 100644
index 735822d..0000000
--- a/erpnext/www/all-products/search.html
+++ /dev/null
@@ -1,46 +0,0 @@
-{% extends "templates/web.html" %}
-
-{% block title %}{{ _('Search') }}{% endblock %}
-
-{%- block head_include %}
- <link rel="stylesheet" href="search.css">
-{% endblock -%}
-
-{% block header %}
-<div class="mb-6">{{ _('Search Products') }}</div>
-{% endblock header %}
-
-{% block page_content %}
-<div class="input-group mb-3">
- <input type="text" name="query" id="search-box" class="form-control" placeholder="Search for products..." aria-label="Product" aria-describedby="button-addon2">
- <div class="input-group-append">
- <button class="btn btn-outline-secondary" type="button" id="search-button">Search</button>
- </div>
-</div>
-
-<!-- To show recent searches -->
-<div class="my-2" id="recent-search-chips"></div>
-
-<div class="row mt-2">
- <!-- Search Results -->
- <div class="col-sm">
- <h2>Products</h2>
- <ul id="results" class="list-group"></ul>
- </div>
-
- {% set show_categories = frappe.db.get_single_value('E Commerce Settings', 'show_categories_in_search_autocomplete') %}
- {% if show_categories %}
- <div id="categories" class="col-sm">
- <h2>Categories</h2>
- <ul id="category-suggestions">
- </ul>
- </div>
- {% endif %}
-
- {% set show_brand_line = frappe.db.get_single_value('E Commerce Settings', 'show_brand_line') %}
- {% if show_brand_line %}
- <span id="show-brand-line"></span>
- {% endif %}
-</div>
-
-{% endblock %}
\ No newline at end of file
diff --git a/erpnext/www/all-products/search.js b/erpnext/www/all-products/search.js
deleted file mode 100644
index e88b576..0000000
--- a/erpnext/www/all-products/search.js
+++ /dev/null
@@ -1,148 +0,0 @@
-let loading = false;
-
-const MAX_RECENT_SEARCHES = 4;
-
-const searchBox = document.getElementById("search-box");
-const searchButton = document.getElementById("search-button");
-const results = document.getElementById("results");
-const categoryList = document.getElementById("category-suggestions");
-const showBrandLine = document.getElementById("show-brand-line");
-const recentSearchArea = document.getElementById("recent-search-chips");
-
-function getRecentSearches() {
- return JSON.parse(localStorage.getItem("recent_searches") || "[]");
-}
-
-function attachEventListenersToChips() {
- const chips = document.getElementsByClassName("recent-chip");
-
- for (let chip of chips) {
- chip.addEventListener("click", () => {
- searchBox.value = chip.innerText;
-
- // Start search with `recent query`
- const event = new Event("input");
- searchBox.dispatchEvent(event);
- searchBox.focus();
- });
- }
-}
-
-function populateRecentSearches() {
- let recents = getRecentSearches();
-
- if (!recents.length) {
- return;
- }
-
- html = "Recent Searches: ";
- for (let query of recents) {
- html += `<button class="btn btn-secondary btn-sm recent-chip mr-1">${query}</button>`;
- }
-
- recentSearchArea.innerHTML = html;
- attachEventListenersToChips();
-}
-
-function populateResults(data) {
- if (!data.message.from_redisearch) {
- // Data not from redisearch
- }
-
- if (data.message.results.length === 0) {
- results.innerHTML = 'No results';
- return;
- }
-
- html = ""
- search_results = data.message.results
- for (let res of search_results) {
- html += `<li class="list-group-item list-group-item-action">
- <img class="item-thumb" src="${res.thumbnail || 'img/placeholder.png'}" />
- <a href="/${res.route}">${res.web_item_name} <span class="brand-line">${showBrandLine && res.brand ? "by " + res.brand : ""}</span></a>
- </li>`
- }
- results.innerHTML = html;
-}
-
-function populateCategoriesList(data) {
- if (!data.message.from_redisearch) {
- // Data not from redisearch
- categoryList.innerHTML = "Install Redisearch to enable autocompletions.";
- return;
- }
-
- if (data.message.results.length === 0) {
- categoryList.innerHTML = 'No results';
- return;
- }
-
- html = ""
- search_results = data.message.results
- for (let category of search_results) {
- html += `<li>${category}</li>`
- }
-
- categoryList.innerHTML = html;
-}
-
-function updateLoadingState() {
- if (loading) {
- results.innerHTML = `<div class="spinner-border"><span class="sr-only">loading...<span></div>`;
- }
-}
-
-searchBox.addEventListener("input", (e) => {
- loading = true;
- updateLoadingState();
- frappe.call({
- method: "erpnext.templates.pages.product_search.search",
- args: {
- query: e.target.value
- },
- callback: (data) => {
- populateResults(data);
- loading = false;
- }
- });
-
- // If there is a suggestion list node
- if (categoryList) {
- frappe.call({
- method: "erpnext.templates.pages.product_search.get_category_suggestions",
- args: {
- query: e.target.value
- },
- callback: (data) => {
- populateCategoriesList(data);
- }
- });
- }
-});
-
-searchButton.addEventListener("click", (e) => {
- let query = searchBox.value;
- if (!query) {
- return;
- }
-
- let recents = getRecentSearches();
-
- if (recents.length >= MAX_RECENT_SEARCHES) {
- // Remove the `First` query
- recents.splice(0, 1);
- }
-
- if (recents.indexOf(query) >= 0) {
- return;
- }
-
- recents.push(query);
-
- localStorage.setItem("recent_searches", JSON.stringify(recents));
-
- // Refresh recent searches
- populateRecentSearches();
-});
-
-populateRecentSearches();
\ No newline at end of file
diff --git a/erpnext/www/shop-by-category/index.py b/erpnext/www/shop-by-category/index.py
index 700d5b2..f94b33e 100644
--- a/erpnext/www/shop-by-category/index.py
+++ b/erpnext/www/shop-by-category/index.py
@@ -71,8 +71,12 @@
fields += ["image"]
categorical_data[category] = frappe.db.sql(f"""
- Select {",".join(fields)}
- from `tab{doctype}`""", as_dict=1)
+ Select
+ {",".join(fields)}
+ from
+ `tab{doctype}`""",
+ as_dict=1
+ )
return categorical_data