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 a795a11b19f..8552d567f33 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 @@ -7,7 +7,7 @@ import frappe 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 @@ -58,13 +58,10 @@ class ECommerceSettings(Document): def validate_search_index_fields(self): if not self.search_index_fields: - return - - # Clean up - # Remove whitespaces + return + 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)): @@ -138,7 +135,7 @@ class ECommerceSettings(Document): old_doc = self.get_doc_before_save() old_fields = old_doc.search_index_fields new_fields = self.search_index_fields - + # if search index fields get changed if not (new_fields == old_fields): create_website_items_index() diff --git a/erpnext/e_commerce/product_grid.js b/erpnext/e_commerce/product_grid.js index a716efa90a9..bd7a568ac0d 100644 --- a/erpnext/e_commerce/product_grid.js +++ b/erpnext/e_commerce/product_grid.js @@ -144,6 +144,8 @@ erpnext.ProductGrid = class { ${ __('Add to Cart') } `; + } 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 3aa6e8c4336..d5ba6f5b299 100644 --- a/erpnext/e_commerce/product_list.js +++ b/erpnext/e_commerce/product_list.js @@ -153,6 +153,8 @@ erpnext.ProductList = class { ${ __('Add to Cart') } `; + } else { + return ``; } } diff --git a/erpnext/e_commerce/product_search.js b/erpnext/e_commerce/product_search.js new file mode 100644 index 00000000000..4f8b028fc1b --- /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(` + + `).find("#search-results-container"); + } + + setupProductsContainer() { + let $products_section = this.search_dropdown.append(` +
+
+ `).find("#product-results"); + + this.products_container = $products_section.append(` +
+
+ ${ __("Type something ...") } +
+
+ `).find("#product-scroll"); + } + + setupCategoryRecentsContainer() { + let $category_recents_section = $("#search-results-container").append(` +
+
+ `).find("#category-recents-container"); + + this.category_container = $category_recents_section.append(` +
+
+ ${ __("Categories") } +
+
+ ${ __('No results') } +
+
+ `).find(".categories"); + + let $recents_section = $("#category-recents-container").append(` +
+
+ ${ __("Recent") } +
+
+ `).find(".recent-searches"); + + this.recents_container = $recents_section.append(` +
+
+ `).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 += ``; + }); + + 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 += ` + + `; + }); + + this.products_container.html(html); + } + + populateCategoriesList(data) { + if (data.message.results.length === 0) { + let empty_html = ` + + ${__('No results')} + + `; + this.category_container.html(empty_html); + return; + } + + let html = "" + let search_results = data.message.results + search_results.forEach((category) => { + html += ` +
+ ${category.name} +
+ `; + }) + + 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 4e1c23c516c..3a2cdaee27a 100644 --- a/erpnext/e_commerce/product_view.js +++ b/erpnext/e_commerce/product_view.js @@ -402,4 +402,4 @@ erpnext.ProductView = class { } 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 3b82a324395..32701013a52 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 @@ ALLOWED_INDEXABLE_FIELDS_SET = { '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 @@ def create_website_items_index(): # 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 @@ def to_search_field(field): 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 @@ def create_web_item_map(website_item_doc): 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 @@ def define_autocomplete_dictionary(): 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_cache_key(name): 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 69db1857dcd..85a75e38b33 100644 --- a/erpnext/public/build.json +++ b/erpnext/public/build.json @@ -71,6 +71,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 6b42987c718..40d58d11f79 100644 --- a/erpnext/public/js/shopping_cart.js +++ b/erpnext/public/js/shopping_cart.js @@ -199,7 +199,7 @@ $.extend(shopping_cart, { }, freeze() { - if (window.location.pathname !== "/cart") return + if (window.location.pathname !== "/cart") return; if (!$('#freeze').length) { let freeze = $('') diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss index 599073cedc7..cfb9a77ef57 100644 --- a/erpnext/public/scss/shopping_cart.scss +++ b/erpnext/public/scss/shopping_cart.scss @@ -423,7 +423,7 @@ body.product-page { .total-discount { font-size: var(--text-base); - color: var(--primary-color); + color: var(--primary-color) !important; } #page-cart { @@ -531,6 +531,7 @@ body.product-page { height: 22px; background-color: var(--gray-200); float: right; + cursor: pointer; } .remove-cart-item-logo { @@ -862,4 +863,15 @@ body.product-page { left: 0; background-color: var(--gray-100); height: 100%; +} + +.item-thumb { + height: 50px; + max-width: 80px; + min-width: 80px; + object-fit: cover; +} + +.brand-line { + color: gray; } \ No newline at end of file diff --git a/erpnext/templates/generators/item_group.html b/erpnext/templates/generators/item_group.html index 72c5d502add..6fa37b0565a 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 %} - +
+
{{ title }}
+ +
+ +
+
{% endblock header %} {% block script %} @@ -19,7 +28,7 @@
- {% if slideshow %} + {% if slideshow %} {{ web_block( "Hero Slider", values=slideshow, @@ -28,8 +37,8 @@ add_bottom_padding=0, ) }} {% endif %} -

{{ title }}

- {% if description %} + + {% if description %}
{{ description or ""}}
{% endif %}
diff --git a/erpnext/templates/includes/cart.js b/erpnext/templates/includes/cart.js index 28fe882dca1..c766dfd0992 100644 --- a/erpnext/templates/includes/cart.js +++ b/erpnext/templates/includes/cart.js @@ -163,7 +163,7 @@ $.extend(shopping_cart, { 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 df8ba075946..a8ea069ef55 100644 --- a/erpnext/templates/pages/product_search.py +++ b/erpnext/templates/pages/product_search.py @@ -6,16 +6,14 @@ from frappe.utils import cstr, nowdate, cint 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, - WEBSITE_ITEM_INDEX, + WEBSITE_ITEM_INDEX, WEBSITE_ITEM_NAME_AUTOCOMPLETE, WEBSITE_ITEM_CATEGORY_AUTOCOMPLETE, make_key ) -# ----------------- no_cache = 1 @@ -35,30 +33,29 @@ def get_product_list(search=None, start=0, limit=12): 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) @@ -80,8 +77,8 @@ def search(query, limit=10, fuzzy_search=True): ac = AutoCompleter(make_key(WEBSITE_ITEM_NAME_AUTOCOMPLETE), conn=red) client = Client(make_key(WEBSITE_ITEM_INDEX), conn=red) suggestions = ac.get_suggestions( - query, - num=limit, + query, + num=limit, fuzzy= fuzzy_search and len(query) > 4 # Fuzzy on length < 3 can be real slow ) @@ -111,11 +108,19 @@ def convert_to_dict(redis_search_doc): @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: @@ -125,5 +130,5 @@ def get_category_suggestions(query): suggestions = ac.get_suggestions(query, num=10) search_results['results'] = [s.string for s in suggestions] - + return search_results \ No newline at end of file diff --git a/erpnext/www/all-products/index.html b/erpnext/www/all-products/index.html index bf84fd51697..667e74c5420 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 %} -
{{ _('Products') }}
+
+
{{ _('Products') }}
+ +
+ +
+
{% endblock header %} {% block page_content %} - - -
@@ -40,11 +24,6 @@
- {% if frappe.form_dict.start or frappe.form_dict.field_filters or frappe.form_dict.attribute_filters or frappe.form_dict.search %} - - - {% endif %} -
{{ _('Filters') }}
diff --git a/erpnext/www/all-products/index.js b/erpnext/www/all-products/index.js index ef8c2105bea..e38514a32a0 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 687532d2eb4..00000000000 --- 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 735822d7659..00000000000 --- a/erpnext/www/all-products/search.html +++ /dev/null @@ -1,46 +0,0 @@ -{% extends "templates/web.html" %} - -{% block title %}{{ _('Search') }}{% endblock %} - -{%- block head_include %} - -{% endblock -%} - -{% block header %} -
{{ _('Search Products') }}
-{% endblock header %} - -{% block page_content %} -
- -
- -
-
- - -
- -
- -
-

Products

-
    -
    - - {% set show_categories = frappe.db.get_single_value('E Commerce Settings', 'show_categories_in_search_autocomplete') %} - {% if show_categories %} -
    -

    Categories

    -
      -
    -
    - {% endif %} - - {% set show_brand_line = frappe.db.get_single_value('E Commerce Settings', 'show_brand_line') %} - {% if show_brand_line %} - - {% endif %} -
    - -{% 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 e88b576c789..00000000000 --- 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 += ``; - } - - 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 += `
  • - - ${res.web_item_name} ${showBrandLine && res.brand ? "by " + res.brand : ""} -
  • ` - } - 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 += `
  • ${category}
  • ` - } - - categoryList.innerHTML = html; -} - -function updateLoadingState() { - if (loading) { - results.innerHTML = `
    loading...
    `; - } -} - -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 700d5b22b8c..f94b33ea850 100644 --- a/erpnext/www/shop-by-category/index.py +++ b/erpnext/www/shop-by-category/index.py @@ -71,8 +71,12 @@ def get_category_records(categories): 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