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
This commit is contained in:
marination
2021-06-01 12:44:49 +05:30
parent dcc79f1bfa
commit b0d7e32018
18 changed files with 369 additions and 342 deletions

View File

@@ -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>

View File

@@ -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'),

View File

@@ -1,9 +0,0 @@
.item-thumb {
height: 50px;
width: 50px;
object-fit: cover;
}
.brand-line {
color: gray;
}

View File

@@ -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 %}

View File

@@ -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();

View File

@@ -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